@agent_memory/dsh 0.2.1 → 0.5.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/client/client.js +500 -16
- package/dist/plugin.mjs +1479 -153
- package/dist/plugin.mjs.map +4 -4
- package/package.json +1 -1
package/dist/plugin.mjs
CHANGED
|
@@ -89,6 +89,7 @@ function memoryPaths(home) {
|
|
|
89
89
|
return {
|
|
90
90
|
home,
|
|
91
91
|
configFile: join(home, "config.toml"),
|
|
92
|
+
projectsFile: join(home, "projects.json"),
|
|
92
93
|
policyFile: join(home, "policy.json"),
|
|
93
94
|
sealFile: join(home, ".state", "policy.seal.json"),
|
|
94
95
|
lockFile: join(home, ".state", "lock"),
|
|
@@ -103,6 +104,9 @@ function memoryPaths(home) {
|
|
|
103
104
|
manifestFile: join(home, ".index", "manifest.json"),
|
|
104
105
|
stateDir: join(home, ".state"),
|
|
105
106
|
journalDir: join(home, ".state", "journal"),
|
|
107
|
+
journalArchiveDir: join(home, ".state", "journal", "archive"),
|
|
108
|
+
checkpointFile: join(home, ".state", "checkpoint.json"),
|
|
109
|
+
checkpointKeyFile: join(home, ".state", "checkpoint.key"),
|
|
106
110
|
txnDir: join(home, ".state", "txn"),
|
|
107
111
|
snapshotsDir: join(home, ".state", "snapshots")
|
|
108
112
|
};
|
|
@@ -553,7 +557,9 @@ var configSchema = {
|
|
|
553
557
|
recall: {
|
|
554
558
|
limit: { type: "int", default: 5, min: 1, max: 100 },
|
|
555
559
|
min_score: { type: "int", default: 500, min: 0, max: 1e3 },
|
|
556
|
-
max_limit: { type: "int", default: 20, min: 1, max: 100 }
|
|
560
|
+
max_limit: { type: "int", default: 20, min: 1, max: 100 },
|
|
561
|
+
hybrid: { type: "bool", default: false },
|
|
562
|
+
embedding_weight: { type: "int", default: 400, min: 0, max: 1e3 }
|
|
557
563
|
},
|
|
558
564
|
distill: {
|
|
559
565
|
max_proposals: { type: "int", default: 5, min: 1, max: 5 },
|
|
@@ -718,6 +724,79 @@ function ensureDir(dir) {
|
|
|
718
724
|
|
|
719
725
|
// packages/core/src/model/frontmatter.ts
|
|
720
726
|
import { createHash } from "node:crypto";
|
|
727
|
+
|
|
728
|
+
// packages/core/src/model/metadata.ts
|
|
729
|
+
var PROJECT_NAME_MAX = 64;
|
|
730
|
+
var TAG_MAX = 32;
|
|
731
|
+
var TAGS_MAX = 8;
|
|
732
|
+
var RELATIONS_MAX = 16;
|
|
733
|
+
var RELATION_TYPES = ["relates_to", "derived_from"];
|
|
734
|
+
var MEMORY_ID = /^mem-\d{8}-[0-9a-f]{8}$/;
|
|
735
|
+
function projectProblem(value) {
|
|
736
|
+
if (value === null || value === void 0) return void 0;
|
|
737
|
+
if (typeof value !== "string") return "project must be a string or null";
|
|
738
|
+
const name2 = value.trim();
|
|
739
|
+
if (name2 === "") return "project must not be empty";
|
|
740
|
+
if (name2 !== value) return "project must not have leading or trailing whitespace";
|
|
741
|
+
if ([...name2].length > PROJECT_NAME_MAX) {
|
|
742
|
+
return `project must be at most ${PROJECT_NAME_MAX} characters`;
|
|
743
|
+
}
|
|
744
|
+
if (/[\r\n]/.test(name2)) return "project must be a single line";
|
|
745
|
+
if (/[\\/]/.test(name2)) return "project must not contain path separators";
|
|
746
|
+
if (name2 === "\u672A\u5F52\u7C7B") return 'project name "\u672A\u5F52\u7C7B" is reserved for the default bucket';
|
|
747
|
+
return void 0;
|
|
748
|
+
}
|
|
749
|
+
function tagsProblem(value) {
|
|
750
|
+
if (!Array.isArray(value)) return "tags must be an array";
|
|
751
|
+
if (value.length > TAGS_MAX) return `tags allows at most ${TAGS_MAX} entries`;
|
|
752
|
+
const seen = /* @__PURE__ */ new Set();
|
|
753
|
+
for (const entry of value) {
|
|
754
|
+
if (typeof entry !== "string") return "each tag must be a string";
|
|
755
|
+
if (entry.length === 0) return "tags must not contain empty entries";
|
|
756
|
+
if ([...entry].length > TAG_MAX) return `each tag must be at most ${TAG_MAX} characters`;
|
|
757
|
+
if (/[\r\n]/.test(entry)) return "tags must be single-line";
|
|
758
|
+
if (seen.has(entry)) return `duplicate tag "${entry}"`;
|
|
759
|
+
seen.add(entry);
|
|
760
|
+
}
|
|
761
|
+
return void 0;
|
|
762
|
+
}
|
|
763
|
+
function relationsProblem(value, selfId) {
|
|
764
|
+
if (!Array.isArray(value)) return "relations must be an array";
|
|
765
|
+
if (value.length > RELATIONS_MAX) return `relations allows at most ${RELATIONS_MAX} entries`;
|
|
766
|
+
const seen = /* @__PURE__ */ new Set();
|
|
767
|
+
for (const entry of value) {
|
|
768
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
769
|
+
return "each relation must be an object";
|
|
770
|
+
}
|
|
771
|
+
const relation = entry;
|
|
772
|
+
if (typeof relation.type !== "string" || !RELATION_TYPES.includes(relation.type)) {
|
|
773
|
+
return "relation.type must be relates_to or derived_from";
|
|
774
|
+
}
|
|
775
|
+
if (typeof relation.target !== "string" || !MEMORY_ID.test(relation.target)) {
|
|
776
|
+
return "relation.target must look like mem-YYYYMMDD-8hex";
|
|
777
|
+
}
|
|
778
|
+
if (relation.target === selfId) return "a relation must not target itself";
|
|
779
|
+
const key = `${relation.type}:${relation.target}`;
|
|
780
|
+
if (seen.has(key)) return "relations must not contain duplicates";
|
|
781
|
+
seen.add(key);
|
|
782
|
+
}
|
|
783
|
+
return void 0;
|
|
784
|
+
}
|
|
785
|
+
function sourceProblem(value) {
|
|
786
|
+
if (value === null || value === void 0) return void 0;
|
|
787
|
+
if (typeof value !== "object" || Array.isArray(value)) return "source must be an object or null";
|
|
788
|
+
const source = value;
|
|
789
|
+
if (typeof source.type !== "string" || source.type.length === 0 || [...source.type].length > 32) {
|
|
790
|
+
return "source.type must be a non-empty string up to 32 characters";
|
|
791
|
+
}
|
|
792
|
+
if (typeof source.id !== "string" || source.id.length === 0 || [...source.id].length > 128) {
|
|
793
|
+
return "source.id must be a non-empty string up to 128 characters";
|
|
794
|
+
}
|
|
795
|
+
if (/[\r\n]/.test(source.type) || /[\r\n]/.test(source.id)) return "source values must be single-line";
|
|
796
|
+
return void 0;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// packages/core/src/model/frontmatter.ts
|
|
721
800
|
var FrontmatterError = class extends Error {
|
|
722
801
|
field;
|
|
723
802
|
filePath;
|
|
@@ -733,6 +812,10 @@ var FIELD_ORDER = [
|
|
|
733
812
|
"mem_schema",
|
|
734
813
|
"kind",
|
|
735
814
|
"scope",
|
|
815
|
+
"project",
|
|
816
|
+
"tags",
|
|
817
|
+
"relations",
|
|
818
|
+
"source",
|
|
736
819
|
"abstract",
|
|
737
820
|
"created_at",
|
|
738
821
|
"valid_from",
|
|
@@ -805,10 +888,32 @@ function parseMemoryFile(text, filePath, limits) {
|
|
|
805
888
|
}
|
|
806
889
|
const id = requireString(values.get("id"), "id", filePath);
|
|
807
890
|
if (!ID.test(id)) fail("id", "must look like mem-YYYYMMDD-8hex", filePath);
|
|
808
|
-
if (values.get("mem_schema") !== 1
|
|
891
|
+
if (values.get("mem_schema") !== 1 && values.get("mem_schema") !== 2) {
|
|
892
|
+
fail("mem_schema", "must be 1 or 2", filePath);
|
|
893
|
+
}
|
|
894
|
+
const memSchema = values.get("mem_schema");
|
|
809
895
|
const kind = requireString(values.get("kind"), "kind", filePath);
|
|
810
896
|
if (!KINDS.has(kind)) fail("kind", "must be fact, pitfall or exp", filePath);
|
|
811
897
|
if (values.get("scope") !== "local") fail("scope", 'must be "local" in V1', filePath);
|
|
898
|
+
const rawProject = values.has("project") ? values.get("project") : null;
|
|
899
|
+
const projectIssue = projectProblem(rawProject);
|
|
900
|
+
if (projectIssue !== void 0) fail("project", projectIssue, filePath);
|
|
901
|
+
const project = rawProject === null || rawProject === void 0 ? null : rawProject;
|
|
902
|
+
const rawTags = values.has("tags") ? values.get("tags") : [];
|
|
903
|
+
const tagsIssue = tagsProblem(rawTags);
|
|
904
|
+
if (tagsIssue !== void 0) fail("tags", tagsIssue, filePath);
|
|
905
|
+
const tags = requireArray(rawTags, "tags", filePath).map((entry) => entry);
|
|
906
|
+
const rawRelations = values.has("relations") ? values.get("relations") : [];
|
|
907
|
+
const relationsIssue = relationsProblem(rawRelations, id);
|
|
908
|
+
if (relationsIssue !== void 0) fail("relations", relationsIssue, filePath);
|
|
909
|
+
const relations = requireArray(rawRelations, "relations", filePath).map((entry) => {
|
|
910
|
+
const relation = entry;
|
|
911
|
+
return { type: relation.type, target: relation.target };
|
|
912
|
+
});
|
|
913
|
+
const rawSource = values.has("source") ? values.get("source") : null;
|
|
914
|
+
const sourceIssue = sourceProblem(rawSource);
|
|
915
|
+
if (sourceIssue !== void 0) fail("source", sourceIssue, filePath);
|
|
916
|
+
const source = rawSource === null || rawSource === void 0 ? null : rawSource;
|
|
812
917
|
const abstract = requireString(values.get("abstract"), "abstract", filePath);
|
|
813
918
|
if (abstract.trim() === "") fail("abstract", "must not be empty", filePath);
|
|
814
919
|
if (/[\r\n]/.test(abstract)) fail("abstract", "must be a single line", filePath);
|
|
@@ -881,9 +986,13 @@ function parseMemoryFile(text, filePath, limits) {
|
|
|
881
986
|
}
|
|
882
987
|
return {
|
|
883
988
|
id,
|
|
884
|
-
mem_schema:
|
|
989
|
+
mem_schema: memSchema,
|
|
885
990
|
kind,
|
|
886
991
|
scope: "local",
|
|
992
|
+
project,
|
|
993
|
+
tags,
|
|
994
|
+
relations,
|
|
995
|
+
source,
|
|
887
996
|
abstract,
|
|
888
997
|
created_at: createdAt,
|
|
889
998
|
valid_from: validFrom,
|
|
@@ -904,6 +1013,10 @@ function serializeMemory(doc) {
|
|
|
904
1013
|
mem_schema: doc.mem_schema,
|
|
905
1014
|
kind: doc.kind,
|
|
906
1015
|
scope: doc.scope,
|
|
1016
|
+
project: doc.project ?? null,
|
|
1017
|
+
tags: doc.tags ?? [],
|
|
1018
|
+
relations: doc.relations ?? [],
|
|
1019
|
+
source: doc.source ?? null,
|
|
907
1020
|
abstract: doc.abstract,
|
|
908
1021
|
created_at: doc.created_at,
|
|
909
1022
|
valid_from: doc.valid_from,
|
|
@@ -1035,17 +1148,17 @@ function loadPolicy(options) {
|
|
|
1035
1148
|
hint: "a human must review it and run `mem policy seal --accept`"
|
|
1036
1149
|
});
|
|
1037
1150
|
const seal2 = parseSeal(sealText, paths.sealFile);
|
|
1038
|
-
const
|
|
1039
|
-
if (seal2.sha256 !==
|
|
1151
|
+
const sha2562 = hashBytes(policyText);
|
|
1152
|
+
if (seal2.sha256 !== sha2562) {
|
|
1040
1153
|
throw new StartupError("POLICY_MISMATCH", "policy.json does not match its sealed hash", {
|
|
1041
1154
|
sourcePath: paths.policyFile,
|
|
1042
|
-
details: [`sealed sha256 ${seal2.sha256}`, `actual sha256 ${
|
|
1155
|
+
details: [`sealed sha256 ${seal2.sha256}`, `actual sha256 ${sha2562}`],
|
|
1043
1156
|
hint: "the policy is immutable at runtime; after a deliberate human edit run `mem policy seal --accept`"
|
|
1044
1157
|
});
|
|
1045
1158
|
}
|
|
1046
1159
|
const { policy, problems } = parsePolicyText(policyText);
|
|
1047
1160
|
if (problems.length > 0) throw policyProblems(paths.policyFile, problems);
|
|
1048
|
-
return { policy, sha256, sealedAt: seal2.sealed_at, policyPath: paths.policyFile, sealPath: paths.sealFile };
|
|
1161
|
+
return { policy, sha256: sha2562, sealedAt: seal2.sealed_at, policyPath: paths.policyFile, sealPath: paths.sealFile };
|
|
1049
1162
|
}
|
|
1050
1163
|
function sealPolicy(options) {
|
|
1051
1164
|
const paths = memoryPaths(options.home);
|
|
@@ -1072,9 +1185,9 @@ function sealPolicy(options) {
|
|
|
1072
1185
|
`;
|
|
1073
1186
|
writeFileAtomic(paths.policyFile, nextText);
|
|
1074
1187
|
const previous = tryReadSeal(paths.sealFile);
|
|
1075
|
-
const
|
|
1076
|
-
writeSeal(paths.sealFile, { policy: "policy.json", sha256, sealed_at: (options.now ?? /* @__PURE__ */ new Date()).toISOString() });
|
|
1077
|
-
return { sealPath: paths.sealFile, sha256, previousSha256: previous?.sha256 ?? null, sealedFiles };
|
|
1188
|
+
const sha2562 = hashBytes(nextText);
|
|
1189
|
+
writeSeal(paths.sealFile, { policy: "policy.json", sha256: sha2562, sealed_at: (options.now ?? /* @__PURE__ */ new Date()).toISOString() });
|
|
1190
|
+
return { sealPath: paths.sealFile, sha256: sha2562, previousSha256: previous?.sha256 ?? null, sealedFiles };
|
|
1078
1191
|
}
|
|
1079
1192
|
function writeSeal(sealPath, record) {
|
|
1080
1193
|
writeFileAtomic(sealPath, `${JSON.stringify(record, null, 2)}
|
|
@@ -1106,15 +1219,15 @@ function parseSeal(text, sealPath) {
|
|
|
1106
1219
|
details: unknown.map((key) => `unknown key "${key}"`)
|
|
1107
1220
|
});
|
|
1108
1221
|
}
|
|
1109
|
-
const
|
|
1110
|
-
if (typeof record.policy !== "string" || typeof
|
|
1222
|
+
const sha2562 = record.sha256;
|
|
1223
|
+
if (typeof record.policy !== "string" || typeof sha2562 !== "string" || !/^[0-9a-f]{64}$/.test(sha2562)) {
|
|
1111
1224
|
throw new StartupError("POLICY_INVALID", "the policy seal file is malformed", {
|
|
1112
1225
|
sourcePath: sealPath,
|
|
1113
1226
|
details: ['expected { "policy": string, "sha256": <64 hex chars>, "sealed_at": string }']
|
|
1114
1227
|
});
|
|
1115
1228
|
}
|
|
1116
1229
|
const sealedAt = typeof record.sealed_at === "string" ? record.sealed_at : "";
|
|
1117
|
-
return { policy: record.policy, sha256, sealed_at: sealedAt };
|
|
1230
|
+
return { policy: record.policy, sha256: sha2562, sealed_at: sealedAt };
|
|
1118
1231
|
}
|
|
1119
1232
|
function tryReadSeal(sealPath) {
|
|
1120
1233
|
let text;
|
|
@@ -1228,9 +1341,204 @@ function missingContentWarnings(paths) {
|
|
|
1228
1341
|
return expected.filter(([filePath]) => !existsSync2(filePath)).map(([, name2]) => `${name2} is not present yet; that injection section will be empty`);
|
|
1229
1342
|
}
|
|
1230
1343
|
|
|
1344
|
+
// packages/core/src/projects/projects.ts
|
|
1345
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1346
|
+
var ProjectError = class extends Error {
|
|
1347
|
+
code;
|
|
1348
|
+
constructor(code, message) {
|
|
1349
|
+
super(message);
|
|
1350
|
+
this.name = "ProjectError";
|
|
1351
|
+
this.code = code;
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
var DESCRIPTION_MAX = 280;
|
|
1355
|
+
var TIMESTAMP2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
1356
|
+
function readRegistry(home) {
|
|
1357
|
+
const file = memoryPaths(home).projectsFile;
|
|
1358
|
+
if (!existsSync3(file)) return { schema: 1, projects: [] };
|
|
1359
|
+
let parsed;
|
|
1360
|
+
try {
|
|
1361
|
+
parsed = JSON.parse(readFileSync3(file, "utf8").replace(/^\uFEFF/, ""));
|
|
1362
|
+
} catch {
|
|
1363
|
+
throw new ProjectError("INVALID", `projects.json is not valid JSON: ${file}`);
|
|
1364
|
+
}
|
|
1365
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1366
|
+
throw new ProjectError("INVALID", "projects.json must be an object");
|
|
1367
|
+
}
|
|
1368
|
+
const record = parsed;
|
|
1369
|
+
if (record.schema !== 1) throw new ProjectError("INVALID", "projects.json schema must be 1");
|
|
1370
|
+
if (!Array.isArray(record.projects)) throw new ProjectError("INVALID", "projects.json projects must be an array");
|
|
1371
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1372
|
+
const projects = [];
|
|
1373
|
+
for (const raw of record.projects) {
|
|
1374
|
+
if (typeof raw !== "object" || raw === null) throw new ProjectError("INVALID", "each project must be an object");
|
|
1375
|
+
const entry = raw;
|
|
1376
|
+
const issue = projectProblem(entry.name);
|
|
1377
|
+
if (issue !== void 0) throw new ProjectError("INVALID", issue);
|
|
1378
|
+
if (typeof entry.description !== "string" || [...entry.description].length > DESCRIPTION_MAX) {
|
|
1379
|
+
throw new ProjectError("INVALID", `project description must be a string up to ${DESCRIPTION_MAX} characters`);
|
|
1380
|
+
}
|
|
1381
|
+
if (typeof entry.created_at !== "string" || !TIMESTAMP2.test(entry.created_at)) {
|
|
1382
|
+
throw new ProjectError("INVALID", "project created_at must be an ISO timestamp");
|
|
1383
|
+
}
|
|
1384
|
+
if (typeof entry.updated_at !== "string" || !TIMESTAMP2.test(entry.updated_at)) {
|
|
1385
|
+
throw new ProjectError("INVALID", "project updated_at must be an ISO timestamp");
|
|
1386
|
+
}
|
|
1387
|
+
const name2 = entry.name;
|
|
1388
|
+
if (seen.has(name2.toLocaleLowerCase())) throw new ProjectError("INVALID", `duplicate project name: ${name2}`);
|
|
1389
|
+
seen.add(name2.toLocaleLowerCase());
|
|
1390
|
+
projects.push({
|
|
1391
|
+
name: name2,
|
|
1392
|
+
description: entry.description,
|
|
1393
|
+
created_at: entry.created_at,
|
|
1394
|
+
updated_at: entry.updated_at
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
return { schema: 1, projects };
|
|
1398
|
+
}
|
|
1399
|
+
function writeRegistry(home, registry) {
|
|
1400
|
+
writeFileAtomic(memoryPaths(home).projectsFile, `${JSON.stringify(registry, null, 2)}
|
|
1401
|
+
`);
|
|
1402
|
+
}
|
|
1403
|
+
function listProjects(home) {
|
|
1404
|
+
return readRegistry(home).projects;
|
|
1405
|
+
}
|
|
1406
|
+
function createProject(options) {
|
|
1407
|
+
const name2 = options.name.trim();
|
|
1408
|
+
const issue = projectProblem(name2);
|
|
1409
|
+
if (issue !== void 0) throw new ProjectError("INVALID", issue);
|
|
1410
|
+
if (name2.length > PROJECT_NAME_MAX) throw new ProjectError("INVALID", "project name is too long");
|
|
1411
|
+
const description = (options.description ?? "").trim();
|
|
1412
|
+
if ([...description].length > DESCRIPTION_MAX) {
|
|
1413
|
+
throw new ProjectError("INVALID", `project description must be at most ${DESCRIPTION_MAX} characters`);
|
|
1414
|
+
}
|
|
1415
|
+
if (/[\r\n]/.test(description)) throw new ProjectError("INVALID", "project description must be a single line");
|
|
1416
|
+
const registry = readRegistry(options.home);
|
|
1417
|
+
if (registry.projects.some((project2) => project2.name.toLocaleLowerCase() === name2.toLocaleLowerCase())) {
|
|
1418
|
+
throw new ProjectError("EXISTS", `project already exists: ${name2}`);
|
|
1419
|
+
}
|
|
1420
|
+
const at = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1421
|
+
const project = { name: name2, description, created_at: at, updated_at: at };
|
|
1422
|
+
writeRegistry(options.home, { schema: 1, projects: [...registry.projects, project] });
|
|
1423
|
+
return project;
|
|
1424
|
+
}
|
|
1425
|
+
function deleteProject(home, name2) {
|
|
1426
|
+
const registry = readRegistry(home);
|
|
1427
|
+
const lower = name2.toLocaleLowerCase();
|
|
1428
|
+
const kept = registry.projects.filter((project) => project.name.toLocaleLowerCase() !== lower);
|
|
1429
|
+
if (kept.length === registry.projects.length) return false;
|
|
1430
|
+
writeRegistry(home, { schema: 1, projects: kept });
|
|
1431
|
+
return true;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// packages/core/src/journal/checkpoint.ts
|
|
1435
|
+
import { createHash as createHash3, createHmac, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
|
|
1436
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1437
|
+
var CHECKPOINT_SCHEMA = 1;
|
|
1438
|
+
function sha256(text) {
|
|
1439
|
+
return createHash3("sha256").update(text, "utf8").digest("hex");
|
|
1440
|
+
}
|
|
1441
|
+
function merkleRoot(leaves) {
|
|
1442
|
+
if (leaves.length === 0) return sha256("");
|
|
1443
|
+
let level = leaves.map((leaf) => sha256(leaf));
|
|
1444
|
+
while (level.length > 1) {
|
|
1445
|
+
const next = [];
|
|
1446
|
+
for (let index = 0; index < level.length; index += 2) {
|
|
1447
|
+
const left = level[index];
|
|
1448
|
+
const right = level[index + 1];
|
|
1449
|
+
next.push(right === void 0 ? left : sha256(`${left}${right}`));
|
|
1450
|
+
}
|
|
1451
|
+
level = next;
|
|
1452
|
+
}
|
|
1453
|
+
return level[0];
|
|
1454
|
+
}
|
|
1455
|
+
function docsRoot(docs) {
|
|
1456
|
+
return merkleRoot(docs.map((doc) => `${doc.id}:${doc.sha256}:${doc.deleted ? "1" : "0"}`).sort());
|
|
1457
|
+
}
|
|
1458
|
+
function readKey(home) {
|
|
1459
|
+
const file = memoryPaths(home).checkpointKeyFile;
|
|
1460
|
+
if (!existsSync4(file)) return void 0;
|
|
1461
|
+
const key = readFileSync4(file);
|
|
1462
|
+
if (key.length !== 32) throw new Error("checkpoint key must be 32 bytes");
|
|
1463
|
+
return key;
|
|
1464
|
+
}
|
|
1465
|
+
function checkpointKey(home) {
|
|
1466
|
+
const existing = readKey(home);
|
|
1467
|
+
if (existing !== void 0) return existing;
|
|
1468
|
+
const file = memoryPaths(home).checkpointKeyFile;
|
|
1469
|
+
const key = randomBytes2(32);
|
|
1470
|
+
try {
|
|
1471
|
+
writeFileSync2(file, key, { flag: "wx", mode: 384 });
|
|
1472
|
+
return key;
|
|
1473
|
+
} catch {
|
|
1474
|
+
const raced = readKey(home);
|
|
1475
|
+
if (raced !== void 0) return raced;
|
|
1476
|
+
throw new Error("checkpoint key is unreadable");
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function canonical(checkpoint) {
|
|
1480
|
+
return JSON.stringify({
|
|
1481
|
+
schema: checkpoint.schema,
|
|
1482
|
+
created_at: checkpoint.created_at,
|
|
1483
|
+
journal: checkpoint.journal,
|
|
1484
|
+
docs: checkpoint.docs,
|
|
1485
|
+
journal_root: checkpoint.journal_root,
|
|
1486
|
+
docs_root: checkpoint.docs_root
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
function writeCheckpoint(home, input) {
|
|
1490
|
+
const docs = input.docs.slice().sort((left, right) => left.id.localeCompare(right.id));
|
|
1491
|
+
const body = {
|
|
1492
|
+
schema: CHECKPOINT_SCHEMA,
|
|
1493
|
+
created_at: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
1494
|
+
journal: input.journal,
|
|
1495
|
+
docs,
|
|
1496
|
+
journal_root: input.journalRoot,
|
|
1497
|
+
docs_root: docsRoot(docs)
|
|
1498
|
+
};
|
|
1499
|
+
const hmac = createHmac("sha256", checkpointKey(home)).update(canonical(body), "utf8").digest("hex");
|
|
1500
|
+
const checkpoint = { ...body, hmac };
|
|
1501
|
+
writeFileAtomic(memoryPaths(home).checkpointFile, `${JSON.stringify(checkpoint, null, 2)}
|
|
1502
|
+
`);
|
|
1503
|
+
return checkpoint;
|
|
1504
|
+
}
|
|
1505
|
+
function readCheckpoint(home) {
|
|
1506
|
+
const file = memoryPaths(home).checkpointFile;
|
|
1507
|
+
if (!existsSync4(file)) return {};
|
|
1508
|
+
let parsed;
|
|
1509
|
+
try {
|
|
1510
|
+
parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
1511
|
+
} catch {
|
|
1512
|
+
return { error: "checkpoint is not valid JSON" };
|
|
1513
|
+
}
|
|
1514
|
+
if (parsed.schema !== CHECKPOINT_SCHEMA) return { error: `unsupported checkpoint schema ${String(parsed.schema)}` };
|
|
1515
|
+
if (typeof parsed.hmac !== "string" || parsed.hmac === "") return { error: "checkpoint has no HMAC" };
|
|
1516
|
+
if (docsRoot(parsed.docs) !== parsed.docs_root) return { error: "checkpoint docs root mismatch" };
|
|
1517
|
+
let key;
|
|
1518
|
+
try {
|
|
1519
|
+
const found = readKey(home);
|
|
1520
|
+
if (found === void 0) return { error: "checkpoint key is missing (restore .state/checkpoint.key or remove checkpoint)" };
|
|
1521
|
+
key = found;
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
1524
|
+
}
|
|
1525
|
+
const expected = createHmac("sha256", key).update(canonical({
|
|
1526
|
+
schema: parsed.schema,
|
|
1527
|
+
created_at: parsed.created_at,
|
|
1528
|
+
journal: parsed.journal,
|
|
1529
|
+
docs: parsed.docs,
|
|
1530
|
+
journal_root: parsed.journal_root,
|
|
1531
|
+
docs_root: parsed.docs_root
|
|
1532
|
+
}), "utf8").digest("hex");
|
|
1533
|
+
const left = Buffer.from(expected, "hex");
|
|
1534
|
+
const right = Buffer.from(parsed.hmac, "hex");
|
|
1535
|
+
if (left.length !== right.length || !timingSafeEqual(left, right)) return { error: "checkpoint HMAC mismatch" };
|
|
1536
|
+
return { checkpoint: parsed };
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1231
1539
|
// packages/core/src/fs/lock.ts
|
|
1232
|
-
import { randomBytes as
|
|
1233
|
-
import { closeSync as closeSync2, fsyncSync as fsyncSync2, openSync as openSync2, readFileSync as
|
|
1540
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
1541
|
+
import { closeSync as closeSync2, fsyncSync as fsyncSync2, openSync as openSync2, readFileSync as readFileSync5, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync3, writeSync } from "node:fs";
|
|
1234
1542
|
var LockError = class extends Error {
|
|
1235
1543
|
code;
|
|
1236
1544
|
constructor(code, message) {
|
|
@@ -1241,7 +1549,7 @@ var LockError = class extends Error {
|
|
|
1241
1549
|
};
|
|
1242
1550
|
function readLock(file) {
|
|
1243
1551
|
try {
|
|
1244
|
-
const raw = JSON.parse(
|
|
1552
|
+
const raw = JSON.parse(readFileSync5(file, "utf8"));
|
|
1245
1553
|
if (typeof raw.pid === "number" && typeof raw.token === "string" && typeof raw.acquired_at === "number" && typeof raw.heartbeat_at === "number") {
|
|
1246
1554
|
return { pid: raw.pid, token: raw.token, acquired_at: raw.acquired_at, heartbeat_at: raw.heartbeat_at };
|
|
1247
1555
|
}
|
|
@@ -1318,7 +1626,7 @@ var FileLock = class {
|
|
|
1318
1626
|
function acquireLock(options) {
|
|
1319
1627
|
const now = options.now ?? Date.now;
|
|
1320
1628
|
const deadline = now() + options.config.timeoutMs;
|
|
1321
|
-
const token =
|
|
1629
|
+
const token = randomBytes3(8).toString("hex");
|
|
1322
1630
|
for (; ; ) {
|
|
1323
1631
|
const info = {
|
|
1324
1632
|
pid: process.pid,
|
|
@@ -1358,7 +1666,7 @@ function acquireLock(options) {
|
|
|
1358
1666
|
}
|
|
1359
1667
|
|
|
1360
1668
|
// packages/core/src/model/ids.ts
|
|
1361
|
-
import { randomBytes as
|
|
1669
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
1362
1670
|
var ID_PATTERN = /^mem-\d{8}-[0-9a-f]{8}$/;
|
|
1363
1671
|
function isMemoryId(value) {
|
|
1364
1672
|
return ID_PATTERN.test(value);
|
|
@@ -1369,7 +1677,7 @@ function formatDate(date) {
|
|
|
1369
1677
|
const day = String(date.getDate()).padStart(2, "0");
|
|
1370
1678
|
return `${year}-${month}-${day}`;
|
|
1371
1679
|
}
|
|
1372
|
-
function newMemoryId(now = /* @__PURE__ */ new Date(), bytes = () =>
|
|
1680
|
+
function newMemoryId(now = /* @__PURE__ */ new Date(), bytes = () => randomBytes4(4)) {
|
|
1373
1681
|
return `mem-${formatDate(now).replaceAll("-", "")}-${bytes().toString("hex")}`;
|
|
1374
1682
|
}
|
|
1375
1683
|
|
|
@@ -1461,13 +1769,26 @@ function supersedeProblems(candidate, docs) {
|
|
|
1461
1769
|
}
|
|
1462
1770
|
|
|
1463
1771
|
// packages/core/src/journal/journal.ts
|
|
1464
|
-
import { createHash as
|
|
1465
|
-
import { closeSync as closeSync3, fsyncSync as fsyncSync3, mkdirSync as mkdirSync2, openSync as openSync3, readFileSync as
|
|
1772
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1773
|
+
import { closeSync as closeSync3, existsSync as existsSync5, fsyncSync as fsyncSync3, mkdirSync as mkdirSync2, openSync as openSync3, readFileSync as readFileSync6, readdirSync, statSync as statSync3, writeSync as writeSync2 } from "node:fs";
|
|
1466
1774
|
import { join as join3 } from "node:path";
|
|
1467
1775
|
var SEGMENT_BYTES = 5 * 1024 * 1024;
|
|
1468
1776
|
var ZERO_HASH = "0".repeat(64);
|
|
1777
|
+
function readArchiveIndex(dir) {
|
|
1778
|
+
const file = join3(dir, "archive", "index.json");
|
|
1779
|
+
if (!existsSync5(file)) return void 0;
|
|
1780
|
+
try {
|
|
1781
|
+
const parsed = JSON.parse(readFileSync6(file, "utf8"));
|
|
1782
|
+
if (parsed.schema !== 1 || typeof parsed.through_seq !== "number" || typeof parsed.through_hash !== "string") {
|
|
1783
|
+
return void 0;
|
|
1784
|
+
}
|
|
1785
|
+
return parsed;
|
|
1786
|
+
} catch {
|
|
1787
|
+
return void 0;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1469
1790
|
function hashLine(line) {
|
|
1470
|
-
return
|
|
1791
|
+
return createHash4("sha256").update(line, "utf8").digest("hex");
|
|
1471
1792
|
}
|
|
1472
1793
|
var Journal = class {
|
|
1473
1794
|
dir;
|
|
@@ -1486,7 +1807,7 @@ var Journal = class {
|
|
|
1486
1807
|
const segments = this.segments();
|
|
1487
1808
|
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
1488
1809
|
const file = join3(this.dir, segments[index]);
|
|
1489
|
-
const lines =
|
|
1810
|
+
const lines = readFileSync6(file, "utf8").split("\n").filter((line) => line !== "");
|
|
1490
1811
|
if (lines.length === 0) continue;
|
|
1491
1812
|
const last = lines[lines.length - 1];
|
|
1492
1813
|
try {
|
|
@@ -1496,20 +1817,22 @@ var Journal = class {
|
|
|
1496
1817
|
return null;
|
|
1497
1818
|
}
|
|
1498
1819
|
}
|
|
1499
|
-
|
|
1820
|
+
const archive = readArchiveIndex(this.dir);
|
|
1821
|
+
return archive === void 0 ? null : { seq: archive.through_seq, hash: archive.through_hash };
|
|
1500
1822
|
}
|
|
1501
1823
|
readAll() {
|
|
1502
1824
|
const entries = [];
|
|
1503
1825
|
const hashes = [];
|
|
1504
1826
|
const issues = [];
|
|
1505
1827
|
const opIds = /* @__PURE__ */ new Set();
|
|
1506
|
-
|
|
1507
|
-
let
|
|
1828
|
+
const archive = readArchiveIndex(this.dir);
|
|
1829
|
+
let expectedSeq = archive === void 0 ? 1 : archive.through_seq + 1;
|
|
1830
|
+
let prev = archive === void 0 ? ZERO_HASH : archive.through_hash;
|
|
1508
1831
|
for (const name2 of this.segments()) {
|
|
1509
1832
|
const file = join3(this.dir, name2);
|
|
1510
1833
|
let text;
|
|
1511
1834
|
try {
|
|
1512
|
-
text =
|
|
1835
|
+
text = readFileSync6(file, "utf8");
|
|
1513
1836
|
} catch (error) {
|
|
1514
1837
|
issues.push({ kind: "parse", message: `cannot read ${name2}: ${String(error)}` });
|
|
1515
1838
|
continue;
|
|
@@ -1526,6 +1849,7 @@ var Journal = class {
|
|
|
1526
1849
|
return { entries, hashes, issues, head: entries.length === 0 ? null : { seq: entries.length, hash: prev } };
|
|
1527
1850
|
}
|
|
1528
1851
|
const hash = hashLine(line);
|
|
1852
|
+
if (archive !== void 0 && entry.seq < expectedSeq) continue;
|
|
1529
1853
|
if (entry.seq !== expectedSeq) {
|
|
1530
1854
|
issues.push({
|
|
1531
1855
|
kind: "seq",
|
|
@@ -1550,7 +1874,92 @@ var Journal = class {
|
|
|
1550
1874
|
prev = hash;
|
|
1551
1875
|
}
|
|
1552
1876
|
}
|
|
1553
|
-
const head = entries.length === 0 ? null : { seq: entries[entries.length - 1]?.seq ?? 0, hash: prev };
|
|
1877
|
+
const head = entries.length === 0 ? archive === void 0 ? null : { seq: archive.through_seq, hash: archive.through_hash } : { seq: entries[entries.length - 1]?.seq ?? 0, hash: prev };
|
|
1878
|
+
return { entries, hashes, issues, head };
|
|
1879
|
+
}
|
|
1880
|
+
/** Cheap operational summary for `mem doctor` / metrics. */
|
|
1881
|
+
stats() {
|
|
1882
|
+
const segments = this.segments();
|
|
1883
|
+
let lines = 0;
|
|
1884
|
+
let bytes = 0;
|
|
1885
|
+
let largest = 0;
|
|
1886
|
+
let firstSeq = null;
|
|
1887
|
+
let lastSeq = null;
|
|
1888
|
+
for (const name2 of segments) {
|
|
1889
|
+
const text = readFileSync6(join3(this.dir, name2), "utf8");
|
|
1890
|
+
const size = Buffer.byteLength(text);
|
|
1891
|
+
bytes += size;
|
|
1892
|
+
largest = Math.max(largest, size);
|
|
1893
|
+
const rows = text.split("\n").filter((line) => line !== "");
|
|
1894
|
+
lines += rows.length;
|
|
1895
|
+
if (firstSeq === null && rows.length > 0) {
|
|
1896
|
+
try {
|
|
1897
|
+
firstSeq = JSON.parse(rows[0]).seq ?? null;
|
|
1898
|
+
} catch {
|
|
1899
|
+
firstSeq = null;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (rows.length > 0) {
|
|
1903
|
+
try {
|
|
1904
|
+
lastSeq = JSON.parse(rows[rows.length - 1]).seq ?? lastSeq;
|
|
1905
|
+
} catch {
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
return { segments: segments.length, lines, bytes, first_seq: firstSeq, last_seq: lastSeq, largest_segment_bytes: largest };
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Replay only entries after a checkpoint. Old lines are skipped without JSON
|
|
1913
|
+
* parsing; the checkpoint's last line hash is the expected `prev`.
|
|
1914
|
+
*/
|
|
1915
|
+
readAfter(afterSeq, prevHash, fromSegment) {
|
|
1916
|
+
if (afterSeq === 0) return this.readAll();
|
|
1917
|
+
const allSegments = this.segments();
|
|
1918
|
+
const start = fromSegment === void 0 ? 0 : allSegments.indexOf(fromSegment);
|
|
1919
|
+
const segments = start < 0 ? allSegments : allSegments.slice(start);
|
|
1920
|
+
const entries = [];
|
|
1921
|
+
const hashes = [];
|
|
1922
|
+
const issues = [];
|
|
1923
|
+
let expectedSeq = afterSeq + 1;
|
|
1924
|
+
let prev = prevHash;
|
|
1925
|
+
let seenTail = false;
|
|
1926
|
+
for (const name2 of segments) {
|
|
1927
|
+
const text = readFileSync6(join3(this.dir, name2), "utf8");
|
|
1928
|
+
for (const line of text.split("\n")) {
|
|
1929
|
+
if (line === "") continue;
|
|
1930
|
+
const seqMatch = /^\{"seq":(\d+)/.exec(line);
|
|
1931
|
+
if (seqMatch !== null && Number(seqMatch[1]) <= afterSeq) continue;
|
|
1932
|
+
let entry;
|
|
1933
|
+
try {
|
|
1934
|
+
entry = JSON.parse(line);
|
|
1935
|
+
} catch {
|
|
1936
|
+
issues.push({ kind: "parse", message: `${name2}: line is not valid JSON`, seq: expectedSeq });
|
|
1937
|
+
return { entries, hashes, issues, head: { seq: afterSeq, hash: prevHash } };
|
|
1938
|
+
}
|
|
1939
|
+
if (entry.seq !== expectedSeq) {
|
|
1940
|
+
issues.push({
|
|
1941
|
+
kind: "seq",
|
|
1942
|
+
message: `${name2}: expected seq ${expectedSeq}, found ${entry.seq}`,
|
|
1943
|
+
seq: entry.seq
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
if (entry.prev !== prev) {
|
|
1947
|
+
issues.push({
|
|
1948
|
+
kind: "chain",
|
|
1949
|
+
message: `${name2}: seq ${entry.seq} prev does not match the checkpoint/previous line`,
|
|
1950
|
+
seq: entry.seq
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
const hash = hashLine(line);
|
|
1954
|
+
if (entry.seq < expectedSeq) continue;
|
|
1955
|
+
entries.push(entry);
|
|
1956
|
+
hashes.push(hash);
|
|
1957
|
+
expectedSeq = entry.seq + 1;
|
|
1958
|
+
prev = hash;
|
|
1959
|
+
seenTail = true;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
const head = seenTail ? { seq: expectedSeq - 1, hash: prev } : { seq: afterSeq, hash: prevHash };
|
|
1554
1963
|
return { entries, hashes, issues, head };
|
|
1555
1964
|
}
|
|
1556
1965
|
findByOpId(opId) {
|
|
@@ -1602,11 +2011,65 @@ var Journal = class {
|
|
|
1602
2011
|
}
|
|
1603
2012
|
return entry;
|
|
1604
2013
|
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Append a batch with one fsync. Bulk import writes files first, then one
|
|
2016
|
+
* journal batch makes the whole import durable. A torn final line is ignored
|
|
2017
|
+
* by recovery just like a single torn append.
|
|
2018
|
+
*/
|
|
2019
|
+
appendBatch(inputs) {
|
|
2020
|
+
if (inputs.length === 0) return [];
|
|
2021
|
+
mkdirSync2(this.dir, { recursive: true });
|
|
2022
|
+
const segments = this.segments();
|
|
2023
|
+
let segmentPath = join3(this.dir, segments[segments.length - 1] ?? "0001.jsonl");
|
|
2024
|
+
let size = 0;
|
|
2025
|
+
try {
|
|
2026
|
+
size = statSync3(segmentPath).size;
|
|
2027
|
+
} catch {
|
|
2028
|
+
size = 0;
|
|
2029
|
+
}
|
|
2030
|
+
let head = this.head();
|
|
2031
|
+
const entries = [];
|
|
2032
|
+
let fd = openSync3(segmentPath, "a");
|
|
2033
|
+
try {
|
|
2034
|
+
for (const input of inputs) {
|
|
2035
|
+
const seq = head === null ? 1 : head.seq + 1;
|
|
2036
|
+
const prev = head === null ? ZERO_HASH : head.hash;
|
|
2037
|
+
const entry = {
|
|
2038
|
+
seq,
|
|
2039
|
+
prev,
|
|
2040
|
+
at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2041
|
+
op: input.op,
|
|
2042
|
+
op_id: input.opId ?? `op-${seq}-${hashLine(`${seq}:${input.op}:${JSON.stringify(input.data)}`).slice(0, 8)}`,
|
|
2043
|
+
data: input.data
|
|
2044
|
+
};
|
|
2045
|
+
const line = JSON.stringify(entry);
|
|
2046
|
+
if (size > 0 && size + Buffer.byteLength(line) + 1 > SEGMENT_BYTES) {
|
|
2047
|
+
closeSync3(fd);
|
|
2048
|
+
const baseName = segmentPath.split(/[\\/]/).pop();
|
|
2049
|
+
const nextNumber = Number(baseName.slice(0, 4)) + 1;
|
|
2050
|
+
segmentPath = join3(this.dir, `${String(nextNumber).padStart(4, "0")}.jsonl`);
|
|
2051
|
+
fd = openSync3(segmentPath, "a");
|
|
2052
|
+
size = 0;
|
|
2053
|
+
}
|
|
2054
|
+
writeSync2(fd, `${line}
|
|
2055
|
+
`);
|
|
2056
|
+
size += Buffer.byteLength(line) + 1;
|
|
2057
|
+
head = { seq, hash: hashLine(line) };
|
|
2058
|
+
entries.push(entry);
|
|
2059
|
+
}
|
|
2060
|
+
fsyncSync3(fd);
|
|
2061
|
+
} finally {
|
|
2062
|
+
closeSync3(fd);
|
|
2063
|
+
}
|
|
2064
|
+
return entries;
|
|
2065
|
+
}
|
|
1605
2066
|
};
|
|
1606
2067
|
|
|
1607
2068
|
// packages/core/src/store/store.ts
|
|
1608
|
-
import { existsSync as
|
|
1609
|
-
import {
|
|
2069
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2070
|
+
import { gzipSync } from "node:zlib";
|
|
2071
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
2072
|
+
import { join as join5 } from "node:path";
|
|
1610
2073
|
|
|
1611
2074
|
// packages/core/src/index/tokenizer.ts
|
|
1612
2075
|
var TOKENIZER_VERSION = 1;
|
|
@@ -1762,6 +2225,116 @@ function search(index, query, options) {
|
|
|
1762
2225
|
}).slice(0, options.limit).map((entry) => ({ id: entry.doc.id, score: Number(entry.normalized.toFixed(6)) }));
|
|
1763
2226
|
}
|
|
1764
2227
|
|
|
2228
|
+
// packages/core/src/index/embedding.ts
|
|
2229
|
+
var DIMENSIONS = 256;
|
|
2230
|
+
function hashFeature(feature) {
|
|
2231
|
+
let hash = 2166136261;
|
|
2232
|
+
for (let index = 0; index < feature.length; index += 1) {
|
|
2233
|
+
hash ^= feature.charCodeAt(index);
|
|
2234
|
+
hash = Math.imul(hash, 16777619);
|
|
2235
|
+
}
|
|
2236
|
+
return hash >>> 0;
|
|
2237
|
+
}
|
|
2238
|
+
var HashEmbeddingProvider = class {
|
|
2239
|
+
name = "hash-256";
|
|
2240
|
+
dimensions = DIMENSIONS;
|
|
2241
|
+
embed(text) {
|
|
2242
|
+
const vector = new Float32Array(DIMENSIONS);
|
|
2243
|
+
const tokens = tokenize(text);
|
|
2244
|
+
const features = [...tokens, ...tokens.map((token) => `#${token}`)];
|
|
2245
|
+
for (const feature of features) {
|
|
2246
|
+
const bucket = hashFeature(feature) % DIMENSIONS;
|
|
2247
|
+
vector[bucket] = (vector[bucket] ?? 0) + 1;
|
|
2248
|
+
}
|
|
2249
|
+
let norm = 0;
|
|
2250
|
+
for (const value of vector) norm += value * value;
|
|
2251
|
+
norm = Math.sqrt(norm);
|
|
2252
|
+
if (norm > 0) for (let index = 0; index < vector.length; index += 1) vector[index] = (vector[index] ?? 0) / norm;
|
|
2253
|
+
return vector;
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
function cosine(left, right) {
|
|
2257
|
+
if (left.length !== right.length) throw new Error("embedding dimensions differ");
|
|
2258
|
+
let dot = 0;
|
|
2259
|
+
for (let index = 0; index < left.length; index += 1) dot += (left[index] ?? 0) * (right[index] ?? 0);
|
|
2260
|
+
return Math.max(0, Math.min(1, dot));
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// packages/core/src/index/hybrid.ts
|
|
2264
|
+
function fuseScores(lexical, vector, embeddingWeightPermille) {
|
|
2265
|
+
const weight = Math.min(1e3, Math.max(0, embeddingWeightPermille)) / 1e3;
|
|
2266
|
+
const ids = /* @__PURE__ */ new Set([...lexical.keys(), ...vector.keys()]);
|
|
2267
|
+
const fused = /* @__PURE__ */ new Map();
|
|
2268
|
+
for (const id of ids) {
|
|
2269
|
+
const lexicalScore = lexical.get(id) ?? 0;
|
|
2270
|
+
const vectorScore = vector.get(id) ?? 0;
|
|
2271
|
+
fused.set(id, (1 - weight) * lexicalScore + weight * vectorScore);
|
|
2272
|
+
}
|
|
2273
|
+
return fused;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// packages/core/src/embed/cache.ts
|
|
2277
|
+
import { mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2278
|
+
import { tmpdir } from "node:os";
|
|
2279
|
+
import { join as join4 } from "node:path";
|
|
2280
|
+
import { spawn } from "node:child_process";
|
|
2281
|
+
var CACHE_SCHEMA = 1;
|
|
2282
|
+
function embeddingsFile(home) {
|
|
2283
|
+
return join4(memoryPaths(home).indexDir, "embeddings.json");
|
|
2284
|
+
}
|
|
2285
|
+
function readEmbeddingCache(home) {
|
|
2286
|
+
try {
|
|
2287
|
+
const parsed = JSON.parse(readFileSync7(embeddingsFile(home), "utf8"));
|
|
2288
|
+
if (parsed.schema !== CACHE_SCHEMA) return void 0;
|
|
2289
|
+
if (typeof parsed.provider !== "string" || typeof parsed.dimensions !== "number") return void 0;
|
|
2290
|
+
if (typeof parsed.generated_at !== "string" || typeof parsed.docs !== "object" || parsed.docs === null) {
|
|
2291
|
+
return void 0;
|
|
2292
|
+
}
|
|
2293
|
+
return parsed;
|
|
2294
|
+
} catch {
|
|
2295
|
+
return void 0;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
function vectorizerCommand() {
|
|
2299
|
+
const command = process.env.AGENT_MEMORY_VECTORIZER?.trim();
|
|
2300
|
+
return command === void 0 || command === "" ? void 0 : command;
|
|
2301
|
+
}
|
|
2302
|
+
async function embedTexts(command, texts) {
|
|
2303
|
+
const dir = mkdtempSync(join4(tmpdir(), "agent-memory-embed-"));
|
|
2304
|
+
const inputPath = join4(dir, "input.json");
|
|
2305
|
+
const outputPath = join4(dir, "output.json");
|
|
2306
|
+
try {
|
|
2307
|
+
writeFileSync4(inputPath, JSON.stringify({ texts }));
|
|
2308
|
+
await new Promise((resolve3, reject) => {
|
|
2309
|
+
const child = spawn(command, [inputPath, outputPath], {
|
|
2310
|
+
stdio: "ignore",
|
|
2311
|
+
shell: true,
|
|
2312
|
+
windowsHide: true
|
|
2313
|
+
});
|
|
2314
|
+
child.once("error", reject);
|
|
2315
|
+
child.once("exit", (code) => {
|
|
2316
|
+
if (code === 0) resolve3();
|
|
2317
|
+
else reject(new Error(`vectorizer exited with ${code ?? "no code"}`));
|
|
2318
|
+
});
|
|
2319
|
+
});
|
|
2320
|
+
const parsed = JSON.parse(readFileSync7(outputPath, "utf8"));
|
|
2321
|
+
if (!Array.isArray(parsed.vectors) || parsed.vectors.length !== texts.length) {
|
|
2322
|
+
throw new Error("vectorizer output must contain one vector per text");
|
|
2323
|
+
}
|
|
2324
|
+
let dimensions = 0;
|
|
2325
|
+
for (const vector of parsed.vectors) {
|
|
2326
|
+
if (!Array.isArray(vector) || vector.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
|
|
2327
|
+
throw new Error("vectorizer vectors must be arrays of finite numbers");
|
|
2328
|
+
}
|
|
2329
|
+
if (dimensions === 0) dimensions = vector.length;
|
|
2330
|
+
if (vector.length !== dimensions) throw new Error("vectorizer vectors have inconsistent dimensions");
|
|
2331
|
+
}
|
|
2332
|
+
return { vectors: parsed.vectors, dimensions };
|
|
2333
|
+
} finally {
|
|
2334
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
1765
2338
|
// packages/core/src/scan/scan.ts
|
|
1766
2339
|
var ScanError = class extends Error {
|
|
1767
2340
|
findings;
|
|
@@ -1841,6 +2414,15 @@ var MemoryStore = class {
|
|
|
1841
2414
|
accounted = /* @__PURE__ */ new Map();
|
|
1842
2415
|
issues = [];
|
|
1843
2416
|
head = null;
|
|
2417
|
+
lastReloadMs = 0;
|
|
2418
|
+
lastCheckpointSeq = 0;
|
|
2419
|
+
checkpointState = null;
|
|
2420
|
+
cachedIndex = null;
|
|
2421
|
+
revision = 0;
|
|
2422
|
+
fileMeta = /* @__PURE__ */ new Map();
|
|
2423
|
+
dirtyDerived = 0;
|
|
2424
|
+
pendingAdds = [];
|
|
2425
|
+
addTimer;
|
|
1844
2426
|
constructor(options) {
|
|
1845
2427
|
this.paths = memoryPaths(options.home);
|
|
1846
2428
|
this.config = options.config;
|
|
@@ -1848,17 +2430,68 @@ var MemoryStore = class {
|
|
|
1848
2430
|
this.beforeStep = options.beforeStep;
|
|
1849
2431
|
this.journal = new Journal(this.paths.journalDir);
|
|
1850
2432
|
this.reload();
|
|
2433
|
+
if (this.journal.stats().lines > 5e4) {
|
|
2434
|
+
try {
|
|
2435
|
+
this.maintain({ force: true });
|
|
2436
|
+
} catch (error) {
|
|
2437
|
+
this.issues.push(`automatic journal maintenance failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
1851
2440
|
}
|
|
1852
|
-
/** Re-read the journal and the memory files; classify each file. */
|
|
2441
|
+
/** Re-read the journal checkpoint and the memory files; classify each file. */
|
|
1853
2442
|
reload() {
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
2443
|
+
const started = performance.now();
|
|
2444
|
+
const previousDocs = this.docs;
|
|
2445
|
+
const previousMeta = this.fileMeta;
|
|
2446
|
+
this.issues = [];
|
|
1857
2447
|
this.docs = /* @__PURE__ */ new Map();
|
|
1858
2448
|
this.quarantined = /* @__PURE__ */ new Map();
|
|
1859
2449
|
this.deleted = /* @__PURE__ */ new Set();
|
|
1860
2450
|
this.accounted = /* @__PURE__ */ new Map();
|
|
2451
|
+
this.revision += 1;
|
|
2452
|
+
this.cachedIndex = null;
|
|
2453
|
+
this.fileMeta = /* @__PURE__ */ new Map();
|
|
1861
2454
|
const latest = /* @__PURE__ */ new Map();
|
|
2455
|
+
const checkpointRead = readCheckpoint(this.paths.home);
|
|
2456
|
+
let read;
|
|
2457
|
+
if (checkpointRead.checkpoint !== void 0) {
|
|
2458
|
+
const checkpoint = checkpointRead.checkpoint;
|
|
2459
|
+
this.lastCheckpointSeq = checkpoint.journal.seq;
|
|
2460
|
+
this.checkpointState = { seq: checkpoint.journal.seq, created_at: checkpoint.created_at };
|
|
2461
|
+
for (const doc of checkpoint.docs) {
|
|
2462
|
+
latest.set(doc.id, { hash: doc.sha256, deleted: doc.deleted });
|
|
2463
|
+
if (doc.deleted) this.deleted.add(doc.id);
|
|
2464
|
+
if (doc.sha256 !== "") this.accounted.set(doc.id, doc.sha256);
|
|
2465
|
+
}
|
|
2466
|
+
const tail = this.journal.readAfter(
|
|
2467
|
+
checkpoint.journal.seq,
|
|
2468
|
+
checkpoint.journal.hash,
|
|
2469
|
+
checkpoint.journal.segment
|
|
2470
|
+
);
|
|
2471
|
+
read = { entries: tail.entries, issues: tail.issues, head: tail.head ?? checkpoint.journal };
|
|
2472
|
+
const journalHead = this.journal.head();
|
|
2473
|
+
if (journalHead !== null && journalHead.seq < checkpoint.journal.seq) {
|
|
2474
|
+
read = {
|
|
2475
|
+
...read,
|
|
2476
|
+
issues: [...read.issues, {
|
|
2477
|
+
kind: "chain",
|
|
2478
|
+
message: `journal is behind checkpoint (#${checkpoint.journal.seq}): possible truncation`
|
|
2479
|
+
}]
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
} else {
|
|
2483
|
+
this.lastCheckpointSeq = 0;
|
|
2484
|
+
this.checkpointState = null;
|
|
2485
|
+
read = this.journal.readAll();
|
|
2486
|
+
if (checkpointRead.error !== void 0) {
|
|
2487
|
+
read = {
|
|
2488
|
+
...read,
|
|
2489
|
+
issues: [...read.issues, { kind: "parse", message: `checkpoint: ${checkpointRead.error}` }]
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
this.issues = read.issues.map((issue) => issue.message);
|
|
2494
|
+
this.head = read.head;
|
|
1862
2495
|
for (const entry of read.entries) {
|
|
1863
2496
|
const id = typeof entry.data.id === "string" ? entry.data.id : void 0;
|
|
1864
2497
|
if (entry.op === "record" || entry.op === "adopt") {
|
|
@@ -1872,9 +2505,10 @@ var MemoryStore = class {
|
|
|
1872
2505
|
}
|
|
1873
2506
|
for (const [id, value] of latest) {
|
|
1874
2507
|
if (value.hash !== "") this.accounted.set(id, value.hash);
|
|
2508
|
+
if (value.deleted) this.deleted.add(id);
|
|
1875
2509
|
}
|
|
1876
2510
|
for (const kind of KINDS2) {
|
|
1877
|
-
const dir =
|
|
2511
|
+
const dir = join5(this.paths.memoriesDir, kind);
|
|
1878
2512
|
let names;
|
|
1879
2513
|
try {
|
|
1880
2514
|
names = readdirSync2(dir).filter((name2) => name2.endsWith(".md"));
|
|
@@ -1882,15 +2516,23 @@ var MemoryStore = class {
|
|
|
1882
2516
|
continue;
|
|
1883
2517
|
}
|
|
1884
2518
|
for (const name2 of names) {
|
|
1885
|
-
const file =
|
|
2519
|
+
const file = join5(dir, name2);
|
|
1886
2520
|
const id = name2.slice(0, -3);
|
|
1887
2521
|
try {
|
|
1888
|
-
const
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
2522
|
+
const stat = statSync4(file);
|
|
2523
|
+
const cachedDoc = previousDocs.get(id);
|
|
2524
|
+
const cachedMeta = previousMeta.get(id);
|
|
2525
|
+
let doc;
|
|
2526
|
+
if (cachedDoc !== void 0 && cachedMeta !== void 0 && cachedMeta.mtimeMs === stat.mtimeMs && cachedMeta.size === stat.size) {
|
|
2527
|
+
doc = cachedDoc;
|
|
2528
|
+
} else {
|
|
2529
|
+
const text = readFileSync8(file, "utf8");
|
|
2530
|
+
doc = parseMemoryFile(text, file, {
|
|
2531
|
+
abstractChars: this.config.limits.abstract_chars,
|
|
2532
|
+
bodyChars: this.config.limits.body_chars,
|
|
2533
|
+
maxRecordChars: this.config.storage.max_record_chars
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
1894
2536
|
if (!isMemoryId(id) || doc.id !== id) {
|
|
1895
2537
|
this.quarantined.set(doc.id || id, "id does not match filename");
|
|
1896
2538
|
continue;
|
|
@@ -1909,6 +2551,7 @@ var MemoryStore = class {
|
|
|
1909
2551
|
continue;
|
|
1910
2552
|
}
|
|
1911
2553
|
this.docs.set(doc.id, doc);
|
|
2554
|
+
this.fileMeta.set(doc.id, { mtimeMs: stat.mtimeMs, size: stat.size });
|
|
1912
2555
|
} catch (error) {
|
|
1913
2556
|
this.quarantined.set(id, `parse: ${error instanceof Error ? error.message : String(error)}`);
|
|
1914
2557
|
}
|
|
@@ -1921,7 +2564,7 @@ var MemoryStore = class {
|
|
|
1921
2564
|
}
|
|
1922
2565
|
}
|
|
1923
2566
|
try {
|
|
1924
|
-
const manifest = JSON.parse(
|
|
2567
|
+
const manifest = JSON.parse(readFileSync8(this.paths.manifestFile, "utf8"));
|
|
1925
2568
|
const expected = manifest.journal_head;
|
|
1926
2569
|
const expectedSeq = expected?.seq;
|
|
1927
2570
|
const expectedHash = expected?.hash;
|
|
@@ -1941,6 +2584,7 @@ var MemoryStore = class {
|
|
|
1941
2584
|
}
|
|
1942
2585
|
} catch {
|
|
1943
2586
|
}
|
|
2587
|
+
this.lastReloadMs = performance.now() - started;
|
|
1944
2588
|
}
|
|
1945
2589
|
active(date = today(this.now())) {
|
|
1946
2590
|
const live = activeDocs(this.docs.values(), date);
|
|
@@ -1961,7 +2605,7 @@ var MemoryStore = class {
|
|
|
1961
2605
|
recall(query, options = {}) {
|
|
1962
2606
|
const text = query.trim();
|
|
1963
2607
|
if (text === "") return { hits: [], warnings: ["empty query"] };
|
|
1964
|
-
this.
|
|
2608
|
+
this.reloadIfJournalChanged();
|
|
1965
2609
|
const date = options.asOf ?? today(this.now());
|
|
1966
2610
|
const limit = Math.min(
|
|
1967
2611
|
options.limit ?? this.config.recall.limit,
|
|
@@ -1970,15 +2614,44 @@ var MemoryStore = class {
|
|
|
1970
2614
|
const activeSet = activeDocs(this.docs.values(), date).filter(
|
|
1971
2615
|
(doc) => !this.deleted.has(doc.id) && !this.quarantined.has(doc.id)
|
|
1972
2616
|
);
|
|
1973
|
-
const index =
|
|
1974
|
-
const found = search(index, text, {
|
|
1975
|
-
limit,
|
|
1976
|
-
minScorePermille: this.config.recall.min_score,
|
|
1977
|
-
...options.kinds === void 0 ? {} : { kinds: options.kinds }
|
|
1978
|
-
});
|
|
2617
|
+
const index = this.indexFor(activeSet, date);
|
|
1979
2618
|
const byId = new Map(activeSet.map((doc) => [doc.id, doc]));
|
|
2619
|
+
const minScore = this.config.recall.min_score / 1e3;
|
|
2620
|
+
let ranked;
|
|
2621
|
+
if (this.config.recall.hybrid) {
|
|
2622
|
+
const lexicalHits = search(index, text, {
|
|
2623
|
+
limit: activeSet.length,
|
|
2624
|
+
minScorePermille: 0,
|
|
2625
|
+
...options.kinds === void 0 ? {} : { kinds: options.kinds }
|
|
2626
|
+
});
|
|
2627
|
+
const lexical = new Map(lexicalHits.map((hit) => [hit.id, hit.score]));
|
|
2628
|
+
const provider = new HashEmbeddingProvider();
|
|
2629
|
+
const queryVector = provider.embed(text);
|
|
2630
|
+
const vector = /* @__PURE__ */ new Map();
|
|
2631
|
+
const kindFilter = options.kinds === void 0 ? void 0 : new Set(options.kinds);
|
|
2632
|
+
for (const doc of activeSet) {
|
|
2633
|
+
if (kindFilter !== void 0 && !kindFilter.has(doc.kind)) continue;
|
|
2634
|
+
vector.set(doc.id, cosine(provider.embed(`${doc.abstract}
|
|
2635
|
+
${doc.body}`), queryVector));
|
|
2636
|
+
}
|
|
2637
|
+
ranked = [...fuseScores(lexical, vector, this.config.recall.embedding_weight).entries()].filter(([, score]) => score >= minScore).map(([id, score]) => ({ id, score })).sort((left, right) => {
|
|
2638
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
2639
|
+
const leftDoc = byId.get(left.id);
|
|
2640
|
+
const rightDoc = byId.get(right.id);
|
|
2641
|
+
if (leftDoc !== void 0 && rightDoc !== void 0 && rightDoc.valid_from !== leftDoc.valid_from) {
|
|
2642
|
+
return rightDoc.valid_from.localeCompare(leftDoc.valid_from);
|
|
2643
|
+
}
|
|
2644
|
+
return left.id.localeCompare(right.id);
|
|
2645
|
+
}).slice(0, limit);
|
|
2646
|
+
} else {
|
|
2647
|
+
ranked = search(index, text, {
|
|
2648
|
+
limit,
|
|
2649
|
+
minScorePermille: this.config.recall.min_score,
|
|
2650
|
+
...options.kinds === void 0 ? {} : { kinds: options.kinds }
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
1980
2653
|
const hits = [];
|
|
1981
|
-
for (const hit of
|
|
2654
|
+
for (const hit of ranked) {
|
|
1982
2655
|
const doc = byId.get(hit.id);
|
|
1983
2656
|
if (doc === void 0) continue;
|
|
1984
2657
|
hits.push({
|
|
@@ -1996,6 +2669,331 @@ var MemoryStore = class {
|
|
|
1996
2669
|
warnings: this.quarantined.size === 0 ? [] : [...this.quarantined.values()].slice(0, 5)
|
|
1997
2670
|
};
|
|
1998
2671
|
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Hybrid retrieval entry point. Sync `recall` stays BM25/hash-only so the
|
|
2674
|
+
* CLI/tests keep a cheap path; the host calls this async version when the
|
|
2675
|
+
* sidecar vector cache exists.
|
|
2676
|
+
*/
|
|
2677
|
+
async recallAsync(query, options = {}) {
|
|
2678
|
+
const text = query.trim();
|
|
2679
|
+
if (text === "" || !this.config.recall.hybrid) return this.recall(query, options);
|
|
2680
|
+
const cache = readEmbeddingCache(this.paths.home);
|
|
2681
|
+
const command = vectorizerCommand();
|
|
2682
|
+
if (cache === void 0 || command === void 0) {
|
|
2683
|
+
const fallback = this.recall(query, options);
|
|
2684
|
+
return {
|
|
2685
|
+
...fallback,
|
|
2686
|
+
warnings: [
|
|
2687
|
+
...fallback.warnings,
|
|
2688
|
+
cache === void 0 ? "hybrid requested but .index/embeddings.json is missing; run `mem embed --apply`" : "hybrid requested but AGENT_MEMORY_VECTORIZER is not set"
|
|
2689
|
+
]
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
try {
|
|
2693
|
+
const queryVector = (await embedTexts(command, [text])).vectors[0];
|
|
2694
|
+
if (queryVector === void 0 || queryVector.length !== cache.dimensions) {
|
|
2695
|
+
throw new Error(`query vector does not match cache dimensions (${cache.dimensions})`);
|
|
2696
|
+
}
|
|
2697
|
+
this.reloadIfJournalChanged();
|
|
2698
|
+
const date = options.asOf ?? today(this.now());
|
|
2699
|
+
const limit = Math.min(options.limit ?? this.config.recall.limit, this.config.recall.max_limit);
|
|
2700
|
+
const activeSet = activeDocs(this.docs.values(), date).filter(
|
|
2701
|
+
(doc) => !this.deleted.has(doc.id) && !this.quarantined.has(doc.id)
|
|
2702
|
+
);
|
|
2703
|
+
const index = this.indexFor(activeSet, date);
|
|
2704
|
+
const lexicalHits = search(index, text, {
|
|
2705
|
+
limit: activeSet.length,
|
|
2706
|
+
minScorePermille: 0,
|
|
2707
|
+
...options.kinds === void 0 ? {} : { kinds: options.kinds }
|
|
2708
|
+
});
|
|
2709
|
+
const lexical = new Map(lexicalHits.map((hit) => [hit.id, hit.score]));
|
|
2710
|
+
const kindFilter = options.kinds === void 0 ? void 0 : new Set(options.kinds);
|
|
2711
|
+
const vector = /* @__PURE__ */ new Map();
|
|
2712
|
+
for (const doc of activeSet) {
|
|
2713
|
+
if (kindFilter !== void 0 && !kindFilter.has(doc.kind)) continue;
|
|
2714
|
+
const cached = cache.docs[doc.id];
|
|
2715
|
+
if (cached === void 0 || cached.length !== queryVector.length) continue;
|
|
2716
|
+
let dot = 0;
|
|
2717
|
+
for (let at = 0; at < queryVector.length; at += 1) dot += (queryVector[at] ?? 0) * (cached[at] ?? 0);
|
|
2718
|
+
vector.set(doc.id, Math.max(0, Math.min(1, dot)));
|
|
2719
|
+
}
|
|
2720
|
+
const minScore = this.config.recall.min_score / 1e3;
|
|
2721
|
+
const byId = new Map(activeSet.map((doc) => [doc.id, doc]));
|
|
2722
|
+
const ranked = [...fuseScores(lexical, vector, this.config.recall.embedding_weight).entries()].filter(([, score]) => score >= minScore).map(([id, score]) => ({ id, score })).sort((left, right) => {
|
|
2723
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
2724
|
+
const leftDoc = byId.get(left.id);
|
|
2725
|
+
const rightDoc = byId.get(right.id);
|
|
2726
|
+
if (leftDoc !== void 0 && rightDoc !== void 0 && rightDoc.valid_from !== leftDoc.valid_from) {
|
|
2727
|
+
return rightDoc.valid_from.localeCompare(leftDoc.valid_from);
|
|
2728
|
+
}
|
|
2729
|
+
return left.id.localeCompare(right.id);
|
|
2730
|
+
}).slice(0, limit);
|
|
2731
|
+
const hits = [];
|
|
2732
|
+
for (const hit of ranked) {
|
|
2733
|
+
const doc = byId.get(hit.id);
|
|
2734
|
+
if (doc === void 0) continue;
|
|
2735
|
+
hits.push({
|
|
2736
|
+
id: doc.id,
|
|
2737
|
+
kind: doc.kind,
|
|
2738
|
+
abstract: doc.abstract,
|
|
2739
|
+
score: hit.score,
|
|
2740
|
+
valid_from: doc.valid_from,
|
|
2741
|
+
valid_until: doc.valid_until,
|
|
2742
|
+
provenance_verifiable: doc.provenance.length > 0
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
return { hits, warnings: this.quarantined.size === 0 ? [] : [...this.quarantined.values()].slice(0, 5) };
|
|
2746
|
+
} catch (error) {
|
|
2747
|
+
const fallback = this.recall(query, options);
|
|
2748
|
+
return {
|
|
2749
|
+
...fallback,
|
|
2750
|
+
warnings: [
|
|
2751
|
+
...fallback.warnings,
|
|
2752
|
+
`hybrid vectorizer failed (${error instanceof Error ? error.message : String(error)}); fell back to lexical`
|
|
2753
|
+
]
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Move records between project labels by rewriting only the `project` field.
|
|
2759
|
+
* This is an organisational change, not a supersede: the id and history stay,
|
|
2760
|
+
* and the journal records the new content hash plus an audit line.
|
|
2761
|
+
*/
|
|
2762
|
+
moveProjects(ids, project) {
|
|
2763
|
+
const projectIssue = projectProblem(project);
|
|
2764
|
+
if (projectIssue !== void 0) throw new MemoryStoreError("INVALID", projectIssue);
|
|
2765
|
+
const lock = acquireLock({
|
|
2766
|
+
file: this.paths.lockFile,
|
|
2767
|
+
config: {
|
|
2768
|
+
heartbeatMs: this.config.lock.heartbeat_ms,
|
|
2769
|
+
staleMs: this.config.lock.stale_ms,
|
|
2770
|
+
timeoutMs: this.config.lock.timeout_ms
|
|
2771
|
+
},
|
|
2772
|
+
now: () => this.now().getTime()
|
|
2773
|
+
});
|
|
2774
|
+
try {
|
|
2775
|
+
this.reload();
|
|
2776
|
+
if (this.issues.length > 0) {
|
|
2777
|
+
throw new MemoryStoreError("CONFLICT", `journal is inconsistent, writes are blocked: ${this.issues[0]}`);
|
|
2778
|
+
}
|
|
2779
|
+
const moved = [];
|
|
2780
|
+
const failed = [];
|
|
2781
|
+
for (const id of ids) {
|
|
2782
|
+
const doc = this.docs.get(id);
|
|
2783
|
+
if (doc === void 0) {
|
|
2784
|
+
failed.push({ id, reason: "not found" });
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
const next = {
|
|
2788
|
+
id: doc.id,
|
|
2789
|
+
mem_schema: doc.mem_schema,
|
|
2790
|
+
kind: doc.kind,
|
|
2791
|
+
scope: doc.scope,
|
|
2792
|
+
project,
|
|
2793
|
+
tags: doc.tags ?? [],
|
|
2794
|
+
relations: doc.relations ?? [],
|
|
2795
|
+
source: doc.source ?? null,
|
|
2796
|
+
abstract: doc.abstract,
|
|
2797
|
+
created_at: doc.created_at,
|
|
2798
|
+
valid_from: doc.valid_from,
|
|
2799
|
+
valid_until: doc.valid_until,
|
|
2800
|
+
supersedes: doc.supersedes,
|
|
2801
|
+
author: doc.author,
|
|
2802
|
+
evidence_level: doc.evidence_level,
|
|
2803
|
+
error_signatures: doc.error_signatures,
|
|
2804
|
+
provenance: doc.provenance,
|
|
2805
|
+
body: doc.body
|
|
2806
|
+
};
|
|
2807
|
+
const text = serializeMemory(next);
|
|
2808
|
+
const file = join5(this.paths.memoriesDir, doc.kind, `${doc.id}.md`);
|
|
2809
|
+
this.beforeStep?.("before-write");
|
|
2810
|
+
writeFileAtomic(file, text);
|
|
2811
|
+
const hash = hashContent(text);
|
|
2812
|
+
this.journal.append({ op: "record", data: { id, sha256: hash } });
|
|
2813
|
+
this.journal.append({ op: "audit", data: { event: "project-move", id, project } });
|
|
2814
|
+
moved.push(id);
|
|
2815
|
+
}
|
|
2816
|
+
this.reload();
|
|
2817
|
+
this.writeDerived();
|
|
2818
|
+
return { moved, failed };
|
|
2819
|
+
} finally {
|
|
2820
|
+
lock.release();
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
/** Update tags/relations in place (organisational metadata, not a revision). */
|
|
2824
|
+
updateMeta(id, patch) {
|
|
2825
|
+
if (patch.tags !== void 0) {
|
|
2826
|
+
const issue = tagsProblem(patch.tags);
|
|
2827
|
+
if (issue !== void 0) throw new MemoryStoreError("INVALID", issue);
|
|
2828
|
+
}
|
|
2829
|
+
if (patch.relations !== void 0) {
|
|
2830
|
+
const issue = relationsProblem(patch.relations, id);
|
|
2831
|
+
if (issue !== void 0) throw new MemoryStoreError("INVALID", issue);
|
|
2832
|
+
}
|
|
2833
|
+
const lock = acquireLock({
|
|
2834
|
+
file: this.paths.lockFile,
|
|
2835
|
+
config: {
|
|
2836
|
+
heartbeatMs: this.config.lock.heartbeat_ms,
|
|
2837
|
+
staleMs: this.config.lock.stale_ms,
|
|
2838
|
+
timeoutMs: this.config.lock.timeout_ms
|
|
2839
|
+
},
|
|
2840
|
+
now: () => this.now().getTime()
|
|
2841
|
+
});
|
|
2842
|
+
try {
|
|
2843
|
+
this.reload();
|
|
2844
|
+
if (this.issues.length > 0) {
|
|
2845
|
+
throw new MemoryStoreError("CONFLICT", `journal is inconsistent, writes are blocked: ${this.issues[0]}`);
|
|
2846
|
+
}
|
|
2847
|
+
const doc = this.docs.get(id);
|
|
2848
|
+
if (doc === void 0) throw new MemoryStoreError("NOT_FOUND", `no memory with id ${id}`);
|
|
2849
|
+
const next = {
|
|
2850
|
+
id: doc.id,
|
|
2851
|
+
mem_schema: doc.mem_schema,
|
|
2852
|
+
kind: doc.kind,
|
|
2853
|
+
scope: doc.scope,
|
|
2854
|
+
project: doc.project ?? null,
|
|
2855
|
+
tags: patch.tags ?? doc.tags ?? [],
|
|
2856
|
+
relations: patch.relations ?? doc.relations ?? [],
|
|
2857
|
+
source: doc.source ?? null,
|
|
2858
|
+
abstract: doc.abstract,
|
|
2859
|
+
created_at: doc.created_at,
|
|
2860
|
+
valid_from: doc.valid_from,
|
|
2861
|
+
valid_until: doc.valid_until,
|
|
2862
|
+
supersedes: doc.supersedes,
|
|
2863
|
+
author: doc.author,
|
|
2864
|
+
evidence_level: doc.evidence_level,
|
|
2865
|
+
error_signatures: doc.error_signatures,
|
|
2866
|
+
provenance: doc.provenance,
|
|
2867
|
+
body: doc.body
|
|
2868
|
+
};
|
|
2869
|
+
const text = serializeMemory(next);
|
|
2870
|
+
writeFileAtomic(join5(this.paths.memoriesDir, doc.kind, `${doc.id}.md`), text);
|
|
2871
|
+
this.journal.append({ op: "record", data: { id, sha256: hashContent(text) } });
|
|
2872
|
+
this.journal.append({ op: "audit", data: { event: "memory-meta", id } });
|
|
2873
|
+
this.reload();
|
|
2874
|
+
this.writeDerived();
|
|
2875
|
+
return this.docs.get(id);
|
|
2876
|
+
} finally {
|
|
2877
|
+
lock.release();
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
/** Force checkpoint + archive old segments; returns what moved. */
|
|
2881
|
+
maintain(options = {}) {
|
|
2882
|
+
const lock = acquireLock({
|
|
2883
|
+
file: this.paths.lockFile,
|
|
2884
|
+
config: {
|
|
2885
|
+
heartbeatMs: this.config.lock.heartbeat_ms,
|
|
2886
|
+
staleMs: this.config.lock.stale_ms,
|
|
2887
|
+
timeoutMs: this.config.lock.timeout_ms
|
|
2888
|
+
},
|
|
2889
|
+
now: () => this.now().getTime()
|
|
2890
|
+
});
|
|
2891
|
+
try {
|
|
2892
|
+
this.reload();
|
|
2893
|
+
if (this.issues.length > 0) {
|
|
2894
|
+
throw new MemoryStoreError("CONFLICT", `journal is inconsistent, repair before maintain: ${this.issues[0]}`);
|
|
2895
|
+
}
|
|
2896
|
+
const checkpointed = this.checkpoint();
|
|
2897
|
+
const archived = this.archiveOldSegments(options.force ?? false);
|
|
2898
|
+
const pruned = this.pruneArchives(options.retentionDays ?? 365);
|
|
2899
|
+
this.reload();
|
|
2900
|
+
return { archived, pruned, checkpointed };
|
|
2901
|
+
} finally {
|
|
2902
|
+
lock.release();
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
archiveOldSegments(force) {
|
|
2906
|
+
if (this.head === null) return [];
|
|
2907
|
+
const read = readCheckpoint(this.paths.home);
|
|
2908
|
+
if (read.checkpoint === void 0) return [];
|
|
2909
|
+
const checkpointSegment = read.checkpoint.journal.segment;
|
|
2910
|
+
if (checkpointSegment === void 0) return [];
|
|
2911
|
+
const segments = this.journal.segments();
|
|
2912
|
+
const at = segments.indexOf(checkpointSegment);
|
|
2913
|
+
if (at <= 0) return [];
|
|
2914
|
+
if (!force && this.journal.stats().lines <= 5e4) return [];
|
|
2915
|
+
const archiveDir = this.paths.journalArchiveDir;
|
|
2916
|
+
ensureDir(archiveDir);
|
|
2917
|
+
const archived = [];
|
|
2918
|
+
const toDelete = [];
|
|
2919
|
+
let throughSeq = 0;
|
|
2920
|
+
let throughHash = "";
|
|
2921
|
+
for (const name2 of segments.slice(0, at)) {
|
|
2922
|
+
const file = join5(this.paths.journalDir, name2);
|
|
2923
|
+
const text = readFileSync8(file, "utf8");
|
|
2924
|
+
const lines = text.split("\n").filter((line) => line !== "");
|
|
2925
|
+
if (lines.length === 0) continue;
|
|
2926
|
+
const hashes = lines.map((line) => hashLine(line));
|
|
2927
|
+
const first = JSON.parse(lines[0]);
|
|
2928
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
2929
|
+
const lastHash = hashLine(lines[lines.length - 1]);
|
|
2930
|
+
const meta = {
|
|
2931
|
+
schema: 1,
|
|
2932
|
+
segment: name2,
|
|
2933
|
+
first_seq: first.seq,
|
|
2934
|
+
last_seq: last.seq,
|
|
2935
|
+
count: lines.length,
|
|
2936
|
+
bytes: Buffer.byteLength(text),
|
|
2937
|
+
sha256: createHash5("sha256").update(text, "utf8").digest("hex"),
|
|
2938
|
+
root: merkleRoot(hashes),
|
|
2939
|
+
archived_at: this.now().toISOString()
|
|
2940
|
+
};
|
|
2941
|
+
const base = name2.replace(/\.jsonl$/, "");
|
|
2942
|
+
writeFileSync5(join5(archiveDir, `${base}.jsonl.gz`), gzipSync(Buffer.from(text)));
|
|
2943
|
+
writeFileAtomic(join5(archiveDir, `${base}.meta.json`), `${JSON.stringify(meta, null, 2)}
|
|
2944
|
+
`);
|
|
2945
|
+
throughSeq = last.seq;
|
|
2946
|
+
throughHash = lastHash;
|
|
2947
|
+
toDelete.push(file);
|
|
2948
|
+
archived.push(name2);
|
|
2949
|
+
}
|
|
2950
|
+
if (archived.length > 0) {
|
|
2951
|
+
writeFileAtomic(
|
|
2952
|
+
join5(archiveDir, "index.json"),
|
|
2953
|
+
`${JSON.stringify({ schema: 1, through_seq: throughSeq, through_hash: throughHash, segments: archived }, null, 2)}
|
|
2954
|
+
`
|
|
2955
|
+
);
|
|
2956
|
+
for (const file of toDelete) rmSync3(file, { force: true });
|
|
2957
|
+
this.journal.append({ op: "audit", data: { event: "journal-archive", segments: archived, through_seq: throughSeq } });
|
|
2958
|
+
}
|
|
2959
|
+
return archived;
|
|
2960
|
+
}
|
|
2961
|
+
pruneArchives(retentionDays) {
|
|
2962
|
+
if (retentionDays <= 0) return [];
|
|
2963
|
+
const dir = this.paths.journalArchiveDir;
|
|
2964
|
+
if (!existsSync6(dir)) return [];
|
|
2965
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
2966
|
+
const pruned = [];
|
|
2967
|
+
for (const name2 of readdirSync2(dir)) {
|
|
2968
|
+
if (!name2.endsWith(".jsonl.gz")) continue;
|
|
2969
|
+
const file = join5(dir, name2);
|
|
2970
|
+
try {
|
|
2971
|
+
const stat = statSync4(file);
|
|
2972
|
+
if (stat.mtimeMs < cutoff) {
|
|
2973
|
+
rmSync3(file, { force: true });
|
|
2974
|
+
pruned.push(name2);
|
|
2975
|
+
}
|
|
2976
|
+
} catch {
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
return pruned;
|
|
2980
|
+
}
|
|
2981
|
+
indexFor(activeSet, asOf) {
|
|
2982
|
+
if (this.cachedIndex !== null && this.cachedIndex.revision === this.revision && this.cachedIndex.asOf === asOf) {
|
|
2983
|
+
return this.cachedIndex.index;
|
|
2984
|
+
}
|
|
2985
|
+
const index = buildIndex(activeSet, TOKENIZER_VERSION);
|
|
2986
|
+
this.cachedIndex = { revision: this.revision, asOf, index };
|
|
2987
|
+
return index;
|
|
2988
|
+
}
|
|
2989
|
+
/** Recall fast path: skip a full reload when the journal head is unchanged. */
|
|
2990
|
+
reloadIfJournalChanged() {
|
|
2991
|
+
const head = this.journal.head();
|
|
2992
|
+
if (this.issues.length === 0 && head !== null && this.head !== null && head.seq === this.head.seq && head.hash === this.head.hash) {
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
this.reload();
|
|
2996
|
+
}
|
|
1999
2997
|
stats() {
|
|
2000
2998
|
return {
|
|
2001
2999
|
active: this.active().length,
|
|
@@ -2047,11 +3045,17 @@ var MemoryStore = class {
|
|
|
2047
3045
|
throw new MemoryStoreError("INVALID", `valid_from must be a real YYYY-MM-DD date, found ${validFrom}`);
|
|
2048
3046
|
}
|
|
2049
3047
|
const duplicateId = this.allocateId(now);
|
|
3048
|
+
const relationIssue = relationsProblem(input.relations ?? [], duplicateId);
|
|
3049
|
+
if (relationIssue !== void 0) throw new MemoryStoreError("INVALID", relationIssue);
|
|
2050
3050
|
const doc = {
|
|
2051
3051
|
id: duplicateId,
|
|
2052
|
-
mem_schema:
|
|
3052
|
+
mem_schema: 2,
|
|
2053
3053
|
kind: input.kind,
|
|
2054
3054
|
scope: "local",
|
|
3055
|
+
project: input.project ?? null,
|
|
3056
|
+
tags: input.tags ?? [],
|
|
3057
|
+
relations: input.relations ?? [],
|
|
3058
|
+
source: input.source ?? null,
|
|
2055
3059
|
abstract: input.abstract,
|
|
2056
3060
|
created_at: now.toISOString(),
|
|
2057
3061
|
valid_from: validFrom,
|
|
@@ -2064,26 +3068,93 @@ var MemoryStore = class {
|
|
|
2064
3068
|
body: input.body
|
|
2065
3069
|
};
|
|
2066
3070
|
const text = serializeMemory(doc);
|
|
2067
|
-
const filePath =
|
|
3071
|
+
const filePath = join5(this.paths.memoriesDir, doc.kind, `${doc.id}.md`);
|
|
2068
3072
|
this.beforeStep?.("before-write");
|
|
2069
|
-
ensureDir(
|
|
3073
|
+
ensureDir(join5(this.paths.memoriesDir, doc.kind));
|
|
2070
3074
|
writeFileAtomic(filePath, text);
|
|
2071
3075
|
this.beforeStep?.("before-journal");
|
|
2072
3076
|
const hash = hashContent(text);
|
|
2073
|
-
this.journal.append({
|
|
3077
|
+
const entry = this.journal.append({
|
|
2074
3078
|
op: "record",
|
|
2075
3079
|
...options.opId === void 0 ? {} : { opId: options.opId },
|
|
2076
3080
|
data: { id: doc.id, sha256: hash }
|
|
2077
3081
|
});
|
|
2078
3082
|
this.beforeStep?.("before-derived");
|
|
2079
|
-
|
|
3083
|
+
const parsed = parseMemoryFile(text, filePath, {
|
|
3084
|
+
abstractChars: this.config.limits.abstract_chars,
|
|
3085
|
+
bodyChars: this.config.limits.body_chars,
|
|
3086
|
+
maxRecordChars: this.config.storage.max_record_chars
|
|
3087
|
+
});
|
|
3088
|
+
this.docs.set(parsed.id, parsed);
|
|
3089
|
+
this.accounted.set(parsed.id, hash);
|
|
3090
|
+
this.deleted.delete(parsed.id);
|
|
3091
|
+
this.head = { seq: entry.seq, hash: hashLine(JSON.stringify(entry)) };
|
|
3092
|
+
this.revision += 1;
|
|
3093
|
+
this.cachedIndex = null;
|
|
3094
|
+
try {
|
|
3095
|
+
const stat = statSync4(filePath);
|
|
3096
|
+
this.fileMeta.set(parsed.id, { mtimeMs: stat.mtimeMs, size: stat.size });
|
|
3097
|
+
} catch {
|
|
3098
|
+
}
|
|
2080
3099
|
this.writeDerived();
|
|
2081
|
-
this.reload();
|
|
2082
3100
|
return { id: doc.id, content_hash: hash, deduplicated: false, warnings: [] };
|
|
2083
3101
|
} finally {
|
|
2084
3102
|
lock.release();
|
|
2085
3103
|
}
|
|
2086
3104
|
}
|
|
3105
|
+
/**
|
|
3106
|
+
* Group-commit variant of `add`: concurrent callers are coalesced for a short
|
|
3107
|
+
* window, then written with one journal fsync. Sequential awaited calls still
|
|
3108
|
+
* pay one commit each; use it when several producers write at once.
|
|
3109
|
+
*/
|
|
3110
|
+
addAsync(input, options) {
|
|
3111
|
+
return new Promise((resolve3, reject) => {
|
|
3112
|
+
this.pendingAdds.push({ input, options, resolve: resolve3, reject });
|
|
3113
|
+
if (this.addTimer === void 0) {
|
|
3114
|
+
this.addTimer = setTimeout(() => {
|
|
3115
|
+
this.addTimer = void 0;
|
|
3116
|
+
this.flushAddGroup();
|
|
3117
|
+
}, 2);
|
|
3118
|
+
}
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
flushAddGroup() {
|
|
3122
|
+
const pending = this.pendingAdds.splice(0);
|
|
3123
|
+
if (pending.length === 0) return;
|
|
3124
|
+
try {
|
|
3125
|
+
this.reload();
|
|
3126
|
+
if (this.issues.length > 0) {
|
|
3127
|
+
throw new MemoryStoreError("CONFLICT", `journal is inconsistent, writes are blocked: ${this.issues[0]}`);
|
|
3128
|
+
}
|
|
3129
|
+
const unique = [];
|
|
3130
|
+
for (const job of pending) {
|
|
3131
|
+
const duplicate = this.findDuplicate(job.input, job.options.author);
|
|
3132
|
+
if (duplicate === void 0) {
|
|
3133
|
+
unique.push(job);
|
|
3134
|
+
} else {
|
|
3135
|
+
this.appendAudit({ event: "dedupe", id: duplicate.id });
|
|
3136
|
+
job.resolve({ id: duplicate.id, content_hash: duplicate.content_hash, deduplicated: true, warnings: [] });
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
for (const author of ["human", "agent"]) {
|
|
3140
|
+
const group = unique.filter((job) => job.options.author === author);
|
|
3141
|
+
if (group.length === 0) continue;
|
|
3142
|
+
const commonOpId = group.length === 1 ? group[0]?.options.opId : void 0;
|
|
3143
|
+
const results = this.addMany(
|
|
3144
|
+
group.map((job) => ({
|
|
3145
|
+
...job.input,
|
|
3146
|
+
...job.options.provenance === void 0 ? {} : { provenance: job.options.provenance }
|
|
3147
|
+
})),
|
|
3148
|
+
{ author, ...commonOpId === void 0 ? {} : { opId: commonOpId } }
|
|
3149
|
+
);
|
|
3150
|
+
for (let index = 0; index < group.length; index += 1) {
|
|
3151
|
+
group[index].resolve(results[index]);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
} catch (error) {
|
|
3155
|
+
for (const job of pending) job.reject(error);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
2087
3158
|
/**
|
|
2088
3159
|
* Accept a hand-placed or hand-edited file after a human reviewed it: append
|
|
2089
3160
|
* an `adopt` journal line with the file's current hash. The file itself is
|
|
@@ -2114,7 +3185,7 @@ var MemoryStore = class {
|
|
|
2114
3185
|
if (file === void 0) {
|
|
2115
3186
|
throw new MemoryStoreError("NOT_FOUND", `no memory file for ${id}`);
|
|
2116
3187
|
}
|
|
2117
|
-
const text =
|
|
3188
|
+
const text = readFileSync8(file, "utf8");
|
|
2118
3189
|
let doc;
|
|
2119
3190
|
try {
|
|
2120
3191
|
doc = parseMemoryFile(text, file, {
|
|
@@ -2237,11 +3308,17 @@ var MemoryStore = class {
|
|
|
2237
3308
|
this.docs
|
|
2238
3309
|
);
|
|
2239
3310
|
if (problems.length > 0) throw new MemoryStoreError("INVALID", problems.join("; "));
|
|
3311
|
+
const relationIssue = relationsProblem(input.relations ?? [], id);
|
|
3312
|
+
if (relationIssue !== void 0) throw new MemoryStoreError("INVALID", relationIssue);
|
|
2240
3313
|
const doc = {
|
|
2241
3314
|
id,
|
|
2242
|
-
mem_schema:
|
|
3315
|
+
mem_schema: 2,
|
|
2243
3316
|
kind: input.kind,
|
|
2244
3317
|
scope: "local",
|
|
3318
|
+
project: input.project ?? null,
|
|
3319
|
+
tags: input.tags ?? [],
|
|
3320
|
+
relations: input.relations ?? [],
|
|
3321
|
+
source: input.source ?? null,
|
|
2245
3322
|
abstract: input.abstract,
|
|
2246
3323
|
created_at: now.toISOString(),
|
|
2247
3324
|
valid_from: input.valid_from ?? today(now),
|
|
@@ -2259,29 +3336,47 @@ var MemoryStore = class {
|
|
|
2259
3336
|
const written = [];
|
|
2260
3337
|
try {
|
|
2261
3338
|
for (const entry of prepared) {
|
|
2262
|
-
const file =
|
|
2263
|
-
ensureDir(
|
|
3339
|
+
const file = join5(this.paths.memoriesDir, entry.kind, `${entry.id}.md`);
|
|
3340
|
+
ensureDir(join5(this.paths.memoriesDir, entry.kind));
|
|
2264
3341
|
writeFileAtomic(file, entry.text);
|
|
2265
3342
|
written.push(file);
|
|
2266
3343
|
}
|
|
2267
3344
|
} catch (error) {
|
|
2268
|
-
for (const file of written)
|
|
3345
|
+
for (const file of written) rmSync3(file, { force: true });
|
|
2269
3346
|
throw error;
|
|
2270
3347
|
}
|
|
2271
3348
|
this.beforeStep?.("before-journal");
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
this.journal.append({
|
|
3349
|
+
const journalEntries = this.journal.appendBatch(
|
|
3350
|
+
prepared.map((entry) => ({
|
|
2275
3351
|
op: "record",
|
|
2276
3352
|
...options.opId === void 0 ? {} : { opId: `${options.opId}-${entry.id}` },
|
|
2277
|
-
data: { id: entry.id, sha256:
|
|
3353
|
+
data: { id: entry.id, sha256: hashContent(entry.text) }
|
|
3354
|
+
}))
|
|
3355
|
+
);
|
|
3356
|
+
for (let index = 0; index < prepared.length; index += 1) {
|
|
3357
|
+
const entry = prepared[index];
|
|
3358
|
+
entry.hash = hashContent(entry.text);
|
|
3359
|
+
const file = join5(this.paths.memoriesDir, entry.kind, `${entry.id}.md`);
|
|
3360
|
+
const parsed = parseMemoryFile(entry.text, file, {
|
|
3361
|
+
abstractChars: this.config.limits.abstract_chars,
|
|
3362
|
+
bodyChars: this.config.limits.body_chars,
|
|
3363
|
+
maxRecordChars: this.config.storage.max_record_chars
|
|
2278
3364
|
});
|
|
2279
|
-
|
|
3365
|
+
this.docs.set(parsed.id, parsed);
|
|
3366
|
+
this.accounted.set(parsed.id, entry.hash);
|
|
3367
|
+
this.deleted.delete(parsed.id);
|
|
3368
|
+
try {
|
|
3369
|
+
const stat = statSync4(file);
|
|
3370
|
+
this.fileMeta.set(parsed.id, { mtimeMs: stat.mtimeMs, size: stat.size });
|
|
3371
|
+
} catch {
|
|
3372
|
+
}
|
|
2280
3373
|
}
|
|
3374
|
+
const lastJournalEntry = journalEntries[journalEntries.length - 1];
|
|
3375
|
+
this.head = { seq: lastJournalEntry.seq, hash: hashLine(JSON.stringify(lastJournalEntry)) };
|
|
2281
3376
|
this.beforeStep?.("before-derived");
|
|
2282
|
-
this.
|
|
3377
|
+
this.revision += 1;
|
|
3378
|
+
this.cachedIndex = null;
|
|
2283
3379
|
this.writeDerived();
|
|
2284
|
-
this.reload();
|
|
2285
3380
|
return prepared.map((entry) => ({
|
|
2286
3381
|
id: entry.id,
|
|
2287
3382
|
content_hash: entry.hash,
|
|
@@ -2341,7 +3436,7 @@ var MemoryStore = class {
|
|
|
2341
3436
|
op: "delete",
|
|
2342
3437
|
data: { id, purged: true, reason: options.reason, sha256: doc?.content_hash ?? "" }
|
|
2343
3438
|
});
|
|
2344
|
-
|
|
3439
|
+
rmSync3(file, { force: true });
|
|
2345
3440
|
let proposals = 0;
|
|
2346
3441
|
let names = [];
|
|
2347
3442
|
try {
|
|
@@ -2350,11 +3445,11 @@ var MemoryStore = class {
|
|
|
2350
3445
|
names = [];
|
|
2351
3446
|
}
|
|
2352
3447
|
for (const name2 of names) {
|
|
2353
|
-
const path =
|
|
3448
|
+
const path = join5(this.paths.pendingDir, name2);
|
|
2354
3449
|
try {
|
|
2355
|
-
const proposal = JSON.parse(
|
|
3450
|
+
const proposal = JSON.parse(readFileSync8(path, "utf8"));
|
|
2356
3451
|
if (Array.isArray(proposal.targets) && proposal.targets.includes(id)) {
|
|
2357
|
-
|
|
3452
|
+
rmSync3(path, { force: true });
|
|
2358
3453
|
this.journal.append({
|
|
2359
3454
|
op: "audit",
|
|
2360
3455
|
data: { event: "proposal-invalidated", proposal: proposal.id ?? name2, target: id }
|
|
@@ -2390,7 +3485,8 @@ var MemoryStore = class {
|
|
|
2390
3485
|
`journal is inconsistent, repair it before rebuild: ${this.issues[0]}`
|
|
2391
3486
|
);
|
|
2392
3487
|
}
|
|
2393
|
-
this.writeDerived();
|
|
3488
|
+
this.writeDerived(true);
|
|
3489
|
+
this.checkpoint();
|
|
2394
3490
|
return {
|
|
2395
3491
|
records: this.accounted.size,
|
|
2396
3492
|
files: this.docs.size,
|
|
@@ -2406,9 +3502,25 @@ var MemoryStore = class {
|
|
|
2406
3502
|
}
|
|
2407
3503
|
/** Quarantine and journal diagnostics, used by `mem doctor`. */
|
|
2408
3504
|
diagnostics() {
|
|
2409
|
-
return {
|
|
3505
|
+
return {
|
|
3506
|
+
quarantined: this.quarantined,
|
|
3507
|
+
issues: this.issues,
|
|
3508
|
+
head: this.head,
|
|
3509
|
+
journal: this.journal.stats(),
|
|
3510
|
+
reload_ms: Number(this.lastReloadMs.toFixed(3)),
|
|
3511
|
+
checkpoint: this.checkpointState
|
|
3512
|
+
};
|
|
2410
3513
|
}
|
|
2411
|
-
writeDerived() {
|
|
3514
|
+
writeDerived(force = false) {
|
|
3515
|
+
if (!force && this.docs.size > 1e3) {
|
|
3516
|
+
this.dirtyDerived += 1;
|
|
3517
|
+
if (this.dirtyDerived < 50) {
|
|
3518
|
+
this.maybeCheckpoint();
|
|
3519
|
+
if (this.head !== null && this.head.seq % 500 === 0) this.maybeArchive();
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
this.dirtyDerived = 0;
|
|
2412
3524
|
const active = this.active();
|
|
2413
3525
|
const manifest = {
|
|
2414
3526
|
schema: 1,
|
|
@@ -2440,11 +3552,49 @@ var MemoryStore = class {
|
|
|
2440
3552
|
for (const doc of sorted) lines.push(`- [${doc.kind}] ${doc.abstract} \u2014 ${doc.id}`);
|
|
2441
3553
|
writeFileAtomic(this.paths.memoryIndexFile, `${lines.join("\n")}
|
|
2442
3554
|
`);
|
|
3555
|
+
this.maybeCheckpoint();
|
|
3556
|
+
this.maybeArchive();
|
|
3557
|
+
}
|
|
3558
|
+
/** Force a checkpoint (maintenance/tests); returns false when journal is empty. */
|
|
3559
|
+
checkpoint() {
|
|
3560
|
+
if (this.head === null) return false;
|
|
3561
|
+
const full = this.journal.readAll();
|
|
3562
|
+
const docs = [...this.accounted.entries()].map(([id, sha2562]) => ({
|
|
3563
|
+
id,
|
|
3564
|
+
sha256: sha2562,
|
|
3565
|
+
deleted: this.deleted.has(id)
|
|
3566
|
+
}));
|
|
3567
|
+
writeCheckpoint(this.paths.home, {
|
|
3568
|
+
journal: {
|
|
3569
|
+
...this.head,
|
|
3570
|
+
segment: this.journal.segments().at(-1)
|
|
3571
|
+
},
|
|
3572
|
+
docs,
|
|
3573
|
+
journalRoot: merkleRoot(full.hashes)
|
|
3574
|
+
});
|
|
3575
|
+
this.lastCheckpointSeq = this.head.seq;
|
|
3576
|
+
return true;
|
|
3577
|
+
}
|
|
3578
|
+
maybeCheckpoint(force = false) {
|
|
3579
|
+
if (this.head === null) return;
|
|
3580
|
+
if (!force && this.head.seq - this.lastCheckpointSeq < 1e3) return;
|
|
3581
|
+
this.checkpoint();
|
|
3582
|
+
}
|
|
3583
|
+
maybeArchive() {
|
|
3584
|
+
if (this.journal.stats().lines <= 5e4) return;
|
|
3585
|
+
if (readCheckpoint(this.paths.home).checkpoint === void 0) this.checkpoint();
|
|
3586
|
+
this.archiveOldSegments(true);
|
|
2443
3587
|
}
|
|
2444
3588
|
validateInput(input) {
|
|
2445
3589
|
if (!KINDS2.includes(input.kind)) {
|
|
2446
3590
|
throw new MemoryStoreError("INVALID", `kind must be one of ${KINDS2.join(", ")}`);
|
|
2447
3591
|
}
|
|
3592
|
+
const projectIssue = projectProblem(input.project ?? null);
|
|
3593
|
+
if (projectIssue !== void 0) throw new MemoryStoreError("INVALID", projectIssue);
|
|
3594
|
+
const tagsIssue = tagsProblem(input.tags ?? []);
|
|
3595
|
+
if (tagsIssue !== void 0) throw new MemoryStoreError("INVALID", tagsIssue);
|
|
3596
|
+
const sourceIssue = sourceProblem(input.source ?? null);
|
|
3597
|
+
if (sourceIssue !== void 0) throw new MemoryStoreError("INVALID", sourceIssue);
|
|
2448
3598
|
if (typeof input.abstract !== "string" || input.abstract.trim() === "") {
|
|
2449
3599
|
throw new MemoryStoreError("INVALID", "abstract must not be empty");
|
|
2450
3600
|
}
|
|
@@ -2490,8 +3640,8 @@ var MemoryStore = class {
|
|
|
2490
3640
|
}
|
|
2491
3641
|
findMemoryFile(id) {
|
|
2492
3642
|
for (const kind of KINDS2) {
|
|
2493
|
-
const file =
|
|
2494
|
-
if (
|
|
3643
|
+
const file = join5(this.paths.memoriesDir, kind, `${id}.md`);
|
|
3644
|
+
if (existsSync6(file)) return file;
|
|
2495
3645
|
}
|
|
2496
3646
|
return void 0;
|
|
2497
3647
|
}
|
|
@@ -2510,7 +3660,7 @@ var MemoryStore = class {
|
|
|
2510
3660
|
allocateId(now) {
|
|
2511
3661
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
2512
3662
|
const id = newMemoryId(now);
|
|
2513
|
-
const exists = this.docs.has(id) || this.accounted.has(id) ||
|
|
3663
|
+
const exists = this.docs.has(id) || this.accounted.has(id) || existsSync6(join5(this.paths.memoriesDir, "fact", `${id}.md`)) || existsSync6(join5(this.paths.memoriesDir, "pitfall", `${id}.md`)) || existsSync6(join5(this.paths.memoriesDir, "exp", `${id}.md`));
|
|
2514
3664
|
if (!exists) return id;
|
|
2515
3665
|
}
|
|
2516
3666
|
throw new MemoryStoreError("CONFLICT", "could not allocate a unique memory id");
|
|
@@ -2525,8 +3675,8 @@ function countPending(dir) {
|
|
|
2525
3675
|
}
|
|
2526
3676
|
|
|
2527
3677
|
// packages/core/src/rules/rules.ts
|
|
2528
|
-
import { existsSync as
|
|
2529
|
-
import { join as
|
|
3678
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9 } from "node:fs";
|
|
3679
|
+
import { join as join6 } from "node:path";
|
|
2530
3680
|
var HEADER = /^(\d+)\.\s*\[(机器门禁|模型指引|废止)\]\s*(.*)$/;
|
|
2531
3681
|
var EXTRA = /^\s+(check|test):\s*(.+)$/;
|
|
2532
3682
|
function parseRules(text) {
|
|
@@ -2581,9 +3731,9 @@ function parseRules(text) {
|
|
|
2581
3731
|
function checkSymbolExists(check, rootDir) {
|
|
2582
3732
|
const [path, symbol] = check.split("#");
|
|
2583
3733
|
if (path === void 0 || symbol === void 0 || path === "" || symbol === "") return false;
|
|
2584
|
-
const file =
|
|
2585
|
-
if (!
|
|
2586
|
-
return
|
|
3734
|
+
const file = join6(rootDir, path);
|
|
3735
|
+
if (!existsSync7(file)) return false;
|
|
3736
|
+
return readFileSync9(file, "utf8").includes(symbol);
|
|
2587
3737
|
}
|
|
2588
3738
|
function validateRules(rules, options) {
|
|
2589
3739
|
const errors = [];
|
|
@@ -2593,12 +3743,12 @@ function validateRules(rules, options) {
|
|
|
2593
3743
|
errors.push(`rule ${rule.number}: check "${rule.check ?? ""}" does not name an existing symbol`);
|
|
2594
3744
|
}
|
|
2595
3745
|
if (rule.test === void 0) continue;
|
|
2596
|
-
const testFile =
|
|
2597
|
-
if (!
|
|
3746
|
+
const testFile = join6(options.rootDir, rule.test);
|
|
3747
|
+
if (!existsSync7(testFile)) {
|
|
2598
3748
|
errors.push(`rule ${rule.number}: test file "${rule.test}" does not exist`);
|
|
2599
3749
|
continue;
|
|
2600
3750
|
}
|
|
2601
|
-
if (!/\[neg\]/.test(
|
|
3751
|
+
if (!/\[neg\]/.test(readFileSync9(testFile, "utf8"))) {
|
|
2602
3752
|
errors.push(`rule ${rule.number}: test file "${rule.test}" has no [neg] case`);
|
|
2603
3753
|
}
|
|
2604
3754
|
}
|
|
@@ -2610,8 +3760,8 @@ function parseAndValidateRules(text, options) {
|
|
|
2610
3760
|
}
|
|
2611
3761
|
function loadRules(home) {
|
|
2612
3762
|
const file = memoryPaths(home).rulesFile;
|
|
2613
|
-
if (!
|
|
2614
|
-
return parseRules(
|
|
3763
|
+
if (!existsSync7(file)) return [];
|
|
3764
|
+
return parseRules(readFileSync9(file, "utf8")).rules;
|
|
2615
3765
|
}
|
|
2616
3766
|
function renderRules(rules, options = {}) {
|
|
2617
3767
|
const lines = [];
|
|
@@ -2624,7 +3774,7 @@ function renderRules(rules, options = {}) {
|
|
|
2624
3774
|
}
|
|
2625
3775
|
function readRulesFile(home) {
|
|
2626
3776
|
const file = memoryPaths(home).rulesFile;
|
|
2627
|
-
return
|
|
3777
|
+
return existsSync7(file) ? readFileSync9(file, "utf8") : "";
|
|
2628
3778
|
}
|
|
2629
3779
|
function saveRulesFile(home, text) {
|
|
2630
3780
|
const parsed = parseAndValidateRules(text, { rootDir: home });
|
|
@@ -2635,20 +3785,20 @@ function saveRulesFile(home, text) {
|
|
|
2635
3785
|
}
|
|
2636
3786
|
|
|
2637
3787
|
// packages/core/src/inject/snapshot.ts
|
|
2638
|
-
import { createHash as
|
|
2639
|
-
import { existsSync as
|
|
2640
|
-
import { join as
|
|
3788
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
3789
|
+
import { existsSync as existsSync8, readFileSync as readFileSync10, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync5 } from "node:fs";
|
|
3790
|
+
import { join as join7 } from "node:path";
|
|
2641
3791
|
var PROMPT_SCHEMA_VERSION = 4;
|
|
2642
3792
|
var SNAPSHOT_KEEP = 20;
|
|
2643
3793
|
function snapshotPath(home, sessionId) {
|
|
2644
|
-
const name2 =
|
|
2645
|
-
return
|
|
3794
|
+
const name2 = createHash6("sha256").update(sessionId, "utf8").digest("hex").slice(0, 32);
|
|
3795
|
+
return join7(memoryPaths(home).snapshotsDir, `${name2}.json`);
|
|
2646
3796
|
}
|
|
2647
3797
|
function readSnapshot(home, sessionId) {
|
|
2648
3798
|
const file = snapshotPath(home, sessionId);
|
|
2649
|
-
if (!
|
|
3799
|
+
if (!existsSync8(file)) return null;
|
|
2650
3800
|
try {
|
|
2651
|
-
const record = JSON.parse(
|
|
3801
|
+
const record = JSON.parse(readFileSync10(file, "utf8"));
|
|
2652
3802
|
if (record.session_id !== sessionId || typeof record.text !== "string" || typeof record.memory_revision !== "string" || typeof record.prompt_schema_version !== "number") {
|
|
2653
3803
|
return null;
|
|
2654
3804
|
}
|
|
@@ -2671,20 +3821,20 @@ function pruneSnapshots(home, keep = SNAPSHOT_KEEP) {
|
|
|
2671
3821
|
return;
|
|
2672
3822
|
}
|
|
2673
3823
|
if (names.length <= keep) return;
|
|
2674
|
-
const byAge = names.map((name2) => ({ name: name2, mtime:
|
|
3824
|
+
const byAge = names.map((name2) => ({ name: name2, mtime: statSync5(join7(dir, name2)).mtimeMs })).sort((left, right) => right.mtime - left.mtime);
|
|
2675
3825
|
for (const entry of byAge.slice(keep)) {
|
|
2676
3826
|
try {
|
|
2677
|
-
|
|
3827
|
+
rmSync4(join7(dir, entry.name), { force: true });
|
|
2678
3828
|
} catch {
|
|
2679
3829
|
}
|
|
2680
3830
|
}
|
|
2681
3831
|
}
|
|
2682
3832
|
|
|
2683
3833
|
// packages/core/src/inject/assemble.ts
|
|
2684
|
-
import { createHash as
|
|
3834
|
+
import { createHash as createHash7, randomBytes as randomBytes5 } from "node:crypto";
|
|
2685
3835
|
|
|
2686
3836
|
// packages/core/src/inject/sections.ts
|
|
2687
|
-
import { existsSync as
|
|
3837
|
+
import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
|
|
2688
3838
|
|
|
2689
3839
|
// packages/core/src/personalization/guidance.ts
|
|
2690
3840
|
function renderGuidance(mode) {
|
|
@@ -2711,7 +3861,7 @@ function truncate(text, budget) {
|
|
|
2711
3861
|
return `${points.slice(0, budget - 1).join("")}\u2026`;
|
|
2712
3862
|
}
|
|
2713
3863
|
function readIfExists(file) {
|
|
2714
|
-
return
|
|
3864
|
+
return existsSync9(file) ? readFileSync11(file, "utf8").replace(COMMENT, "").trim() : "";
|
|
2715
3865
|
}
|
|
2716
3866
|
var SECTION_ORDERS = { SOUL: 100, GUIDANCE: 110, RULES: 150, USER: 200, MEMORY: 300 };
|
|
2717
3867
|
function builtinSectionSources(context) {
|
|
@@ -2799,7 +3949,7 @@ function charCount2(text) {
|
|
|
2799
3949
|
}
|
|
2800
3950
|
function memoryRevision(docs) {
|
|
2801
3951
|
const lines = [...docs].map((doc) => `${doc.id}:${doc.content_hash}`).sort((left, right) => left.localeCompare(right));
|
|
2802
|
-
return
|
|
3952
|
+
return createHash7("sha256").update(lines.join("\n"), "utf8").digest("hex");
|
|
2803
3953
|
}
|
|
2804
3954
|
function catalogLines(docs) {
|
|
2805
3955
|
const sorted = [...docs].sort((left, right) => {
|
|
@@ -2882,7 +4032,7 @@ ${section.text}`).join("\n\n");
|
|
|
2882
4032
|
throw new InjectionError("OVER_BUDGET", `injection is over inject.total_max_chars (${input.config.inject.total_max_chars})`);
|
|
2883
4033
|
}
|
|
2884
4034
|
}
|
|
2885
|
-
const delimiter = input.delimiter ??
|
|
4035
|
+
const delimiter = input.delimiter ?? randomBytes5(8).toString("hex");
|
|
2886
4036
|
const escaped = body.split(delimiter).join("<escaped>");
|
|
2887
4037
|
const text = `<<AM-${delimiter}>>
|
|
2888
4038
|
${escaped}
|
|
@@ -2932,8 +4082,8 @@ ${escaped}
|
|
|
2932
4082
|
}
|
|
2933
4083
|
|
|
2934
4084
|
// packages/core/src/proposals/proposals.ts
|
|
2935
|
-
import { existsSync as
|
|
2936
|
-
import { join as
|
|
4085
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync12, rmSync as rmSync5 } from "node:fs";
|
|
4086
|
+
import { join as join8 } from "node:path";
|
|
2937
4087
|
var ProposalError = class extends Error {
|
|
2938
4088
|
code;
|
|
2939
4089
|
constructor(code, message) {
|
|
@@ -2946,7 +4096,7 @@ function pendingDir(store) {
|
|
|
2946
4096
|
return store.paths.pendingDir;
|
|
2947
4097
|
}
|
|
2948
4098
|
function proposalPath(store, id) {
|
|
2949
|
-
return
|
|
4099
|
+
return join8(pendingDir(store), `${id}.json`);
|
|
2950
4100
|
}
|
|
2951
4101
|
function listProposals(store) {
|
|
2952
4102
|
let names;
|
|
@@ -2958,7 +4108,7 @@ function listProposals(store) {
|
|
|
2958
4108
|
const proposals = [];
|
|
2959
4109
|
for (const name2 of names) {
|
|
2960
4110
|
try {
|
|
2961
|
-
proposals.push(JSON.parse(
|
|
4111
|
+
proposals.push(JSON.parse(readFileSync12(join8(pendingDir(store), name2), "utf8")));
|
|
2962
4112
|
} catch {
|
|
2963
4113
|
}
|
|
2964
4114
|
}
|
|
@@ -2966,8 +4116,8 @@ function listProposals(store) {
|
|
|
2966
4116
|
}
|
|
2967
4117
|
function readProposal(store, id) {
|
|
2968
4118
|
const file = proposalPath(store, id);
|
|
2969
|
-
if (!
|
|
2970
|
-
return JSON.parse(
|
|
4119
|
+
if (!existsSync10(file)) throw new ProposalError("NOT_FOUND", `no pending proposal ${id}`);
|
|
4120
|
+
return JSON.parse(readFileSync12(file, "utf8"));
|
|
2971
4121
|
}
|
|
2972
4122
|
function currentHash(store, id) {
|
|
2973
4123
|
const doc = store.read(id);
|
|
@@ -3033,7 +4183,7 @@ function decideProposal(deps, input) {
|
|
|
3033
4183
|
const proposal = readProposal(store, input.id);
|
|
3034
4184
|
if (input.decision === "reject") {
|
|
3035
4185
|
store.appendJournal("decide", { proposal: proposal.id, decision: "reject", reason: input.reason ?? "" });
|
|
3036
|
-
|
|
4186
|
+
rmSync5(proposalPath(store, proposal.id), { force: true });
|
|
3037
4187
|
return;
|
|
3038
4188
|
}
|
|
3039
4189
|
if (proposal.op === "delete") {
|
|
@@ -3044,13 +4194,13 @@ function decideProposal(deps, input) {
|
|
|
3044
4194
|
}
|
|
3045
4195
|
store.remove(target, { reason: proposal.reason, baseHash: hash });
|
|
3046
4196
|
store.journal.append({ op: "decide", data: { proposal: proposal.id, decision: "accept" } });
|
|
3047
|
-
|
|
4197
|
+
rmSync5(proposalPath(store, proposal.id), { force: true });
|
|
3048
4198
|
return;
|
|
3049
4199
|
}
|
|
3050
4200
|
if (proposal.op === "create") {
|
|
3051
4201
|
store.addMany(proposal.replacements, { author: "human", opId: `decide-${proposal.id}` });
|
|
3052
4202
|
store.appendJournal("decide", { proposal: proposal.id, decision: "accept" });
|
|
3053
|
-
|
|
4203
|
+
rmSync5(proposalPath(store, proposal.id), { force: true });
|
|
3054
4204
|
return;
|
|
3055
4205
|
}
|
|
3056
4206
|
proposal.targets.forEach((target, index) => {
|
|
@@ -3063,11 +4213,11 @@ function decideProposal(deps, input) {
|
|
|
3063
4213
|
{ author: "human", opId: `decide-${proposal.id}` }
|
|
3064
4214
|
);
|
|
3065
4215
|
store.appendJournal("decide", { proposal: proposal.id, decision: "accept" });
|
|
3066
|
-
|
|
4216
|
+
rmSync5(proposalPath(store, proposal.id), { force: true });
|
|
3067
4217
|
}
|
|
3068
4218
|
|
|
3069
4219
|
// packages/core/src/actor.ts
|
|
3070
|
-
import { createHash as
|
|
4220
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
3071
4221
|
var EvidenceError = class extends Error {
|
|
3072
4222
|
constructor(message) {
|
|
3073
4223
|
super(message);
|
|
@@ -3087,8 +4237,8 @@ function bindEvidence(input) {
|
|
|
3087
4237
|
if (!event.text.includes(input.quote)) {
|
|
3088
4238
|
throw new EvidenceError("quote is not a substring of the referenced event");
|
|
3089
4239
|
}
|
|
3090
|
-
const
|
|
3091
|
-
if (input.expectedSha256 !== void 0 && input.expectedSha256 !==
|
|
4240
|
+
const sha2562 = createHash8("sha256").update(event.text, "utf8").digest("hex");
|
|
4241
|
+
if (input.expectedSha256 !== void 0 && input.expectedSha256 !== sha2562) {
|
|
3092
4242
|
throw new EvidenceError("event hash does not match the resolved event");
|
|
3093
4243
|
}
|
|
3094
4244
|
return {
|
|
@@ -3096,7 +4246,7 @@ function bindEvidence(input) {
|
|
|
3096
4246
|
event_seq: input.eventSeq,
|
|
3097
4247
|
speaker: event.speaker,
|
|
3098
4248
|
quote: input.quote,
|
|
3099
|
-
event_sha256:
|
|
4249
|
+
event_sha256: sha2562
|
|
3100
4250
|
};
|
|
3101
4251
|
}
|
|
3102
4252
|
|
|
@@ -3120,14 +4270,14 @@ var SessionQuota = class {
|
|
|
3120
4270
|
};
|
|
3121
4271
|
|
|
3122
4272
|
// packages/core/src/distill/closeout.ts
|
|
3123
|
-
import { createHash as
|
|
3124
|
-
import { existsSync as
|
|
3125
|
-
import { join as
|
|
4273
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
4274
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
|
|
4275
|
+
import { join as join9 } from "node:path";
|
|
3126
4276
|
function loadRejected(home) {
|
|
3127
|
-
const file =
|
|
3128
|
-
if (!
|
|
4277
|
+
const file = join9(memoryPaths(home).stateDir, "distill-rejected.json");
|
|
4278
|
+
if (!existsSync11(file)) return /* @__PURE__ */ new Set();
|
|
3129
4279
|
try {
|
|
3130
|
-
const parsed = JSON.parse(
|
|
4280
|
+
const parsed = JSON.parse(readFileSync13(file, "utf8"));
|
|
3131
4281
|
return new Set(Array.isArray(parsed.hashes) ? parsed.hashes.filter((value) => typeof value === "string") : []);
|
|
3132
4282
|
} catch {
|
|
3133
4283
|
return /* @__PURE__ */ new Set();
|
|
@@ -3137,7 +4287,7 @@ function addRejected(home, hashes) {
|
|
|
3137
4287
|
const set = loadRejected(home);
|
|
3138
4288
|
for (const hash of hashes) set.add(hash);
|
|
3139
4289
|
writeFileAtomic(
|
|
3140
|
-
|
|
4290
|
+
join9(memoryPaths(home).stateDir, "distill-rejected.json"),
|
|
3141
4291
|
`${JSON.stringify({ hashes: [...set].sort() }, null, 2)}
|
|
3142
4292
|
`
|
|
3143
4293
|
);
|
|
@@ -3211,7 +4361,7 @@ function summaryHash(candidate) {
|
|
|
3211
4361
|
[...candidate.targets].sort().join(","),
|
|
3212
4362
|
...candidate.replacements.map((replacement) => normalizedAbstract(replacement.abstract))
|
|
3213
4363
|
].join("|");
|
|
3214
|
-
return
|
|
4364
|
+
return createHash9("sha256").update(material, "utf8").digest("hex");
|
|
3215
4365
|
}
|
|
3216
4366
|
function findJson(text) {
|
|
3217
4367
|
const start = text.indexOf("{");
|
|
@@ -3380,9 +4530,9 @@ var CloseoutRound = class {
|
|
|
3380
4530
|
};
|
|
3381
4531
|
|
|
3382
4532
|
// packages/core/src/ops/ops.ts
|
|
3383
|
-
import { createHash as
|
|
3384
|
-
import { cpSync, existsSync as
|
|
3385
|
-
import { join as
|
|
4533
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
4534
|
+
import { cpSync, existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync14, readdirSync as readdirSync5, statSync as statSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4535
|
+
import { join as join10, relative } from "node:path";
|
|
3386
4536
|
var INCLUDE = [
|
|
3387
4537
|
"config.toml",
|
|
3388
4538
|
"policy.json",
|
|
@@ -3399,9 +4549,9 @@ function exportStore(options) {
|
|
|
3399
4549
|
const paths = memoryPaths(options.home);
|
|
3400
4550
|
mkdirSync3(options.out, { recursive: true });
|
|
3401
4551
|
for (const entry of INCLUDE) {
|
|
3402
|
-
const source =
|
|
3403
|
-
if (!
|
|
3404
|
-
const target =
|
|
4552
|
+
const source = join10(paths.home, entry);
|
|
4553
|
+
if (!existsSync12(source)) continue;
|
|
4554
|
+
const target = join10(options.out, entry);
|
|
3405
4555
|
cpSync(source, target, { recursive: true });
|
|
3406
4556
|
}
|
|
3407
4557
|
const lines = [];
|
|
@@ -3409,22 +4559,22 @@ function exportStore(options) {
|
|
|
3409
4559
|
let bytes = 0;
|
|
3410
4560
|
const walk2 = (dir, base) => {
|
|
3411
4561
|
for (const name2 of readdirSync5(dir)) {
|
|
3412
|
-
const full =
|
|
3413
|
-
if (
|
|
4562
|
+
const full = join10(dir, name2);
|
|
4563
|
+
if (statSync6(full).isDirectory()) {
|
|
3414
4564
|
walk2(full, base);
|
|
3415
4565
|
continue;
|
|
3416
4566
|
}
|
|
3417
4567
|
const rel = relative(base, full).replaceAll("\\", "/");
|
|
3418
|
-
const data =
|
|
3419
|
-
lines.push(`${
|
|
4568
|
+
const data = readFileSync14(full);
|
|
4569
|
+
lines.push(`${createHash10("sha256").update(data).digest("hex")} ${rel}`);
|
|
3420
4570
|
files += 1;
|
|
3421
4571
|
bytes += data.length;
|
|
3422
4572
|
}
|
|
3423
4573
|
};
|
|
3424
4574
|
walk2(options.out, options.out);
|
|
3425
4575
|
lines.sort();
|
|
3426
|
-
const sums =
|
|
3427
|
-
|
|
4576
|
+
const sums = join10(options.out, "SHA256SUMS");
|
|
4577
|
+
writeFileSync6(sums, `${lines.join("\n")}
|
|
3428
4578
|
`, "utf8");
|
|
3429
4579
|
return { out: options.out, files, bytes, sums };
|
|
3430
4580
|
}
|
|
@@ -3432,8 +4582,8 @@ function personalizationChars(home) {
|
|
|
3432
4582
|
const paths = memoryPaths(home);
|
|
3433
4583
|
const charsOf = (file) => {
|
|
3434
4584
|
try {
|
|
3435
|
-
if (!
|
|
3436
|
-
return [...
|
|
4585
|
+
if (!existsSync12(file)) return 0;
|
|
4586
|
+
return [...readFileSync14(file, "utf8").replace(/<!--[\s\S]*?-->/g, "")].length;
|
|
3437
4587
|
} catch {
|
|
3438
4588
|
return 0;
|
|
3439
4589
|
}
|
|
@@ -3474,7 +4624,7 @@ function inspectStore(options) {
|
|
|
3474
4624
|
}
|
|
3475
4625
|
|
|
3476
4626
|
// packages/core/src/profile/profile.ts
|
|
3477
|
-
import { existsSync as
|
|
4627
|
+
import { existsSync as existsSync13, readFileSync as readFileSync15 } from "node:fs";
|
|
3478
4628
|
var PROFILE_SEPARATOR = "\xA7";
|
|
3479
4629
|
var PROFILE_MAX_CHARS = 1e4;
|
|
3480
4630
|
var ProfileError = class extends Error {
|
|
@@ -3534,7 +4684,7 @@ function validateProfileEntries(entries) {
|
|
|
3534
4684
|
}
|
|
3535
4685
|
function loadProfile(home) {
|
|
3536
4686
|
const file = memoryPaths(home).userFile;
|
|
3537
|
-
const text =
|
|
4687
|
+
const text = existsSync13(file) ? readFileSync15(file, "utf8") : "";
|
|
3538
4688
|
const entries = parseProfileEntries(text);
|
|
3539
4689
|
return { entries, chars: profileChars(entries) };
|
|
3540
4690
|
}
|
|
@@ -3563,7 +4713,7 @@ function findProfileEntry(entries, oldText) {
|
|
|
3563
4713
|
}
|
|
3564
4714
|
|
|
3565
4715
|
// packages/core/src/personalization/scaffold.ts
|
|
3566
|
-
import { existsSync as
|
|
4716
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
3567
4717
|
var SOUL_TEMPLATE = `<!--
|
|
3568
4718
|
\u4EBA\u683C\u6458\u8981\uFF1A\u6CE8\u5165\u65F6\u53EA\u53D6\u524D 200 \u5B57\u3002\u7528\u6563\u6587\u5199\u300C\u4F60\u662F\u8C01\u3001\u600E\u4E48\u8BF4\u8BDD\u3001\u8FB9\u754C\u5728\u54EA\u300D\uFF0C\u4E0D\u8981\u5199\u4E8B\u5B9E\u6761\u76EE\u3002
|
|
3569
4719
|
\u793A\u4F8B\uFF1A\u4F60\u7684\u89D2\u8272\u662F\u2026\u2026\uFF0C\u4EE5\u2026\u2026\u7684\u8EAB\u4EFD\u4E0E\u7528\u6237\u4EA4\u6D41\uFF1B\u8BED\u6C14\u2026\u2026\uFF1B\u4E0D\u786E\u5B9A\u65F6\u5148\u95EE\uFF0C\u4E0D\u66FF\u7528\u6237\u505A\u51B3\u5B9A\u3002
|
|
@@ -3595,7 +4745,7 @@ function scaffoldPersonalization(options) {
|
|
|
3595
4745
|
const created = [];
|
|
3596
4746
|
const kept = [];
|
|
3597
4747
|
for (const [file, template] of targets) {
|
|
3598
|
-
if (
|
|
4748
|
+
if (existsSync14(file)) {
|
|
3599
4749
|
kept.push(file);
|
|
3600
4750
|
continue;
|
|
3601
4751
|
}
|
|
@@ -3965,8 +5115,8 @@ var LEVELS = /* @__PURE__ */ new Set(["user_explicit", "user_confirmed", "observ
|
|
|
3965
5115
|
function importBundle(store, bundle) {
|
|
3966
5116
|
if (typeof bundle !== "object" || bundle === null) throw new Error("bundle must be an object");
|
|
3967
5117
|
const record = bundle;
|
|
3968
|
-
if (record.format !== "agent-memory-export-v1") {
|
|
3969
|
-
throw new Error("unsupported bundle format (expected agent-memory-export-v1)");
|
|
5118
|
+
if (record.format !== "agent-memory-export-v1" && record.format !== "agent-memory-export-v2") {
|
|
5119
|
+
throw new Error("unsupported bundle format (expected agent-memory-export-v1 or v2)");
|
|
3970
5120
|
}
|
|
3971
5121
|
if (!Array.isArray(record.memories)) throw new Error("bundle.memories must be an array");
|
|
3972
5122
|
let imported = 0;
|
|
@@ -3988,6 +5138,14 @@ function importBundle(store, bundle) {
|
|
|
3988
5138
|
kind: raw.kind,
|
|
3989
5139
|
abstract,
|
|
3990
5140
|
body,
|
|
5141
|
+
...typeof raw.project === "string" || raw.project === null ? { project: raw.project } : {},
|
|
5142
|
+
...Array.isArray(raw.tags) ? { tags: raw.tags.filter((entry) => typeof entry === "string") } : {},
|
|
5143
|
+
...Array.isArray(raw.relations) ? {
|
|
5144
|
+
relations: raw.relations.filter(
|
|
5145
|
+
(entry) => typeof entry === "object" && entry !== null && typeof entry.type === "string" && typeof entry.target === "string"
|
|
5146
|
+
)
|
|
5147
|
+
} : {},
|
|
5148
|
+
...typeof raw.source === "object" && raw.source !== null && typeof raw.source.type === "string" && typeof raw.source.id === "string" ? { source: { type: raw.source.type, id: raw.source.id } } : {},
|
|
3991
5149
|
evidence_level: evidenceLevel,
|
|
3992
5150
|
...typeof raw.valid_from === "string" ? { valid_from: raw.valid_from } : {},
|
|
3993
5151
|
...Array.isArray(raw.error_signatures) ? { error_signatures: raw.error_signatures.filter((entry) => typeof entry === "string") } : {}
|
|
@@ -4037,6 +5195,8 @@ var TOOL_SCHEMAS = {
|
|
|
4037
5195
|
)
|
|
4038
5196
|
},
|
|
4039
5197
|
valid_from: { type: "string" },
|
|
5198
|
+
project: { type: "string" },
|
|
5199
|
+
tags: { type: "array", items: { type: "string" }, maxItems: 8 },
|
|
4040
5200
|
error_signatures: { type: "array", items: { type: "string" }, maxItems: 3 }
|
|
4041
5201
|
},
|
|
4042
5202
|
["kind", "abstract", "body", "evidence"]
|
|
@@ -4063,6 +5223,8 @@ var TOOL_SCHEMAS = {
|
|
|
4063
5223
|
)
|
|
4064
5224
|
},
|
|
4065
5225
|
valid_from: { type: "string" },
|
|
5226
|
+
project: { type: "string" },
|
|
5227
|
+
tags: { type: "array", items: { type: "string" }, maxItems: 8 },
|
|
4066
5228
|
error_signatures: { type: "array", items: { type: "string" }, maxItems: 3 }
|
|
4067
5229
|
},
|
|
4068
5230
|
["kind", "abstract", "body", "evidence"]
|
|
@@ -4804,6 +5966,10 @@ function summary(doc, serial) {
|
|
|
4804
5966
|
id: doc.id,
|
|
4805
5967
|
kind: doc.kind,
|
|
4806
5968
|
abstract: doc.abstract,
|
|
5969
|
+
project: doc.project ?? null,
|
|
5970
|
+
tags: doc.tags ?? [],
|
|
5971
|
+
relations: doc.relations ?? [],
|
|
5972
|
+
source: doc.source ?? null,
|
|
4807
5973
|
author: doc.author,
|
|
4808
5974
|
valid_from: doc.valid_from,
|
|
4809
5975
|
valid_until: doc.valid_until,
|
|
@@ -4837,8 +6003,8 @@ function buildItem(store, id) {
|
|
|
4837
6003
|
content_hash: doc.content_hash
|
|
4838
6004
|
};
|
|
4839
6005
|
}
|
|
4840
|
-
function buildSearch(store, query, limit = 20) {
|
|
4841
|
-
const result = store.
|
|
6006
|
+
async function buildSearch(store, query, limit = 20) {
|
|
6007
|
+
const result = await store.recallAsync(query, { limit });
|
|
4842
6008
|
const serials = serialsFor(store.active());
|
|
4843
6009
|
return {
|
|
4844
6010
|
query,
|
|
@@ -4853,6 +6019,73 @@ function buildSearch(store, query, limit = 20) {
|
|
|
4853
6019
|
warnings: result.warnings
|
|
4854
6020
|
};
|
|
4855
6021
|
}
|
|
6022
|
+
function buildGraph(store, home) {
|
|
6023
|
+
const active = store.active();
|
|
6024
|
+
const serials = serialsFor(active);
|
|
6025
|
+
const registered = listProjects(home).map((project) => project.name);
|
|
6026
|
+
const registeredSet = new Set(registered);
|
|
6027
|
+
const effectiveProject = (doc) => doc.project !== null && doc.project !== void 0 && registeredSet.has(doc.project) ? doc.project : "\u672A\u5F52\u7C7B";
|
|
6028
|
+
const names = new Set(registered);
|
|
6029
|
+
for (const doc of active) names.add(effectiveProject(doc));
|
|
6030
|
+
const projects = [...names].map((name2) => {
|
|
6031
|
+
const docs = active.filter((doc) => effectiveProject(doc) === name2);
|
|
6032
|
+
const last = docs.map((doc) => doc.valid_from).sort().at(-1) ?? null;
|
|
6033
|
+
return { name: name2, count: docs.length, last_updated: last };
|
|
6034
|
+
});
|
|
6035
|
+
projects.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
|
|
6036
|
+
const limited = active.slice().sort((left, right) => right.valid_from.localeCompare(left.valid_from) || right.created_at.localeCompare(left.created_at)).slice(0, 200);
|
|
6037
|
+
const ids = new Set(limited.map((doc) => doc.id));
|
|
6038
|
+
const nodes = limited.map((doc) => {
|
|
6039
|
+
const serial = serials.get(doc.id) ?? "";
|
|
6040
|
+
return {
|
|
6041
|
+
id: doc.id,
|
|
6042
|
+
serial,
|
|
6043
|
+
display: displayName(serial, doc.abstract),
|
|
6044
|
+
kind: doc.kind,
|
|
6045
|
+
project: effectiveProject(doc),
|
|
6046
|
+
tags: doc.tags ?? [],
|
|
6047
|
+
relations: (doc.relations ?? []).map((relation) => ({ type: relation.type, target: relation.target })),
|
|
6048
|
+
supersedes: doc.supersedes,
|
|
6049
|
+
valid_from: doc.valid_from
|
|
6050
|
+
};
|
|
6051
|
+
});
|
|
6052
|
+
const edges = [];
|
|
6053
|
+
for (const doc of limited) {
|
|
6054
|
+
for (const target of doc.supersedes) {
|
|
6055
|
+
if (ids.has(target)) edges.push({ from: doc.id, to: target, type: "supersedes" });
|
|
6056
|
+
}
|
|
6057
|
+
for (const relation of doc.relations ?? []) {
|
|
6058
|
+
if (ids.has(relation.target)) edges.push({ from: doc.id, to: relation.target, type: relation.type });
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
const conflicts = [];
|
|
6062
|
+
for (let left = 0; left < limited.length; left += 1) {
|
|
6063
|
+
for (let right = left + 1; right < limited.length; right += 1) {
|
|
6064
|
+
const a = limited[left];
|
|
6065
|
+
const b = limited[right];
|
|
6066
|
+
if (effectiveProject(a) !== effectiveProject(b)) continue;
|
|
6067
|
+
const shared = (a.tags ?? []).some((tag) => (b.tags ?? []).includes(tag));
|
|
6068
|
+
if (!shared) continue;
|
|
6069
|
+
const charsA = /* @__PURE__ */ new Set([...a.abstract.replace(/\s+/g, "")]);
|
|
6070
|
+
const charsB = /* @__PURE__ */ new Set([...b.abstract.replace(/\s+/g, "")]);
|
|
6071
|
+
let overlap = 0;
|
|
6072
|
+
for (const char of charsA) if (charsB.has(char)) overlap += 1;
|
|
6073
|
+
const similarity = overlap / Math.max(1, (/* @__PURE__ */ new Set([...charsA, ...charsB])).size);
|
|
6074
|
+
if (similarity >= 0.8) {
|
|
6075
|
+
conflicts.push({ a: a.id, b: b.id, project: effectiveProject(a), reason: "\u540C\u9879\u76EE\u540C\u6807\u7B7E\u4E14\u6458\u8981\u9AD8\u5EA6\u91CD\u53E0" });
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
return {
|
|
6080
|
+
schema: 1,
|
|
6081
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6082
|
+
truncated: active.length > limited.length,
|
|
6083
|
+
projects,
|
|
6084
|
+
nodes,
|
|
6085
|
+
edges,
|
|
6086
|
+
conflicts
|
|
6087
|
+
};
|
|
6088
|
+
}
|
|
4856
6089
|
function exportBundle(store) {
|
|
4857
6090
|
const active = store.active();
|
|
4858
6091
|
const serials = serialsFor(active);
|
|
@@ -4864,7 +6097,7 @@ function exportBundle(store) {
|
|
|
4864
6097
|
content_hash: doc.content_hash
|
|
4865
6098
|
}));
|
|
4866
6099
|
return {
|
|
4867
|
-
format: "agent-memory-export-
|
|
6100
|
+
format: "agent-memory-export-v2",
|
|
4868
6101
|
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4869
6102
|
memories,
|
|
4870
6103
|
proposals: listProposals(store).map((proposal) => ({
|
|
@@ -4974,7 +6207,11 @@ function saveRulesGui(home, store, text) {
|
|
|
4974
6207
|
function json(value, status = 200) {
|
|
4975
6208
|
return new Response(JSON.stringify(value), {
|
|
4976
6209
|
status,
|
|
4977
|
-
headers: {
|
|
6210
|
+
headers: {
|
|
6211
|
+
"content-type": "application/json; charset=utf-8",
|
|
6212
|
+
"cache-control": "no-store",
|
|
6213
|
+
"x-agent-memory-api-version": "1"
|
|
6214
|
+
}
|
|
4978
6215
|
});
|
|
4979
6216
|
}
|
|
4980
6217
|
function errorResponse(message, status = 400) {
|
|
@@ -5012,6 +6249,87 @@ function guiRoutes(deps) {
|
|
|
5012
6249
|
requestBody: "buffered",
|
|
5013
6250
|
fetch: async () => json(buildList(deps.store))
|
|
5014
6251
|
};
|
|
6252
|
+
const graph = {
|
|
6253
|
+
path: "/api/agent-memory/graph",
|
|
6254
|
+
methods: ["GET"],
|
|
6255
|
+
requestBody: "buffered",
|
|
6256
|
+
fetch: async () => json(buildGraph(deps.store, deps.home))
|
|
6257
|
+
};
|
|
6258
|
+
const projects = {
|
|
6259
|
+
path: "/api/agent-memory/projects",
|
|
6260
|
+
methods: ["GET", "POST", "DELETE"],
|
|
6261
|
+
requestBody: "buffered",
|
|
6262
|
+
fetch: async (request) => {
|
|
6263
|
+
if (request.method === "GET") return json({ projects: listProjects(deps.home) });
|
|
6264
|
+
if (request.method === "DELETE") {
|
|
6265
|
+
const name2 = new URL(request.url).searchParams.get("name") ?? "";
|
|
6266
|
+
if (name2 === "") return errorResponse("name is required");
|
|
6267
|
+
return deleteProject(deps.home, name2) ? json({ ok: true }) : errorResponse("project not found", 404);
|
|
6268
|
+
}
|
|
6269
|
+
const body = await readJson(request);
|
|
6270
|
+
if (typeof body.name !== "string" || body.name.trim() === "") return errorResponse("name is required");
|
|
6271
|
+
try {
|
|
6272
|
+
const project = createProject({
|
|
6273
|
+
home: deps.home,
|
|
6274
|
+
name: body.name,
|
|
6275
|
+
...typeof body.description === "string" ? { description: body.description } : {}
|
|
6276
|
+
});
|
|
6277
|
+
return json({ project });
|
|
6278
|
+
} catch (error) {
|
|
6279
|
+
return errorResponse(error instanceof Error ? error.message : String(error), 409);
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
};
|
|
6283
|
+
const moveProject = {
|
|
6284
|
+
path: "/api/agent-memory/projects/move",
|
|
6285
|
+
methods: ["POST"],
|
|
6286
|
+
requestBody: "buffered",
|
|
6287
|
+
fetch: async (request) => {
|
|
6288
|
+
const body = await readJson(request);
|
|
6289
|
+
if (!Array.isArray(body.ids) || body.ids.some((id) => typeof id !== "string")) {
|
|
6290
|
+
return errorResponse("ids must be a string array");
|
|
6291
|
+
}
|
|
6292
|
+
if (body.project !== null && typeof body.project !== "string") {
|
|
6293
|
+
return errorResponse("project must be a string or null");
|
|
6294
|
+
}
|
|
6295
|
+
const target = body.project;
|
|
6296
|
+
if (target !== null && !listProjects(deps.home).some((project) => project.name === target)) {
|
|
6297
|
+
return errorResponse(`unknown project: ${target}`, 409);
|
|
6298
|
+
}
|
|
6299
|
+
try {
|
|
6300
|
+
const result = deps.store.moveProjects(body.ids, target);
|
|
6301
|
+
return json({ ok: true, ...result });
|
|
6302
|
+
} catch (error) {
|
|
6303
|
+
return errorResponse(error instanceof Error ? error.message : String(error), 409);
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
};
|
|
6307
|
+
const meta = {
|
|
6308
|
+
path: "/api/agent-memory/meta",
|
|
6309
|
+
methods: ["POST"],
|
|
6310
|
+
requestBody: "buffered",
|
|
6311
|
+
fetch: async (request) => {
|
|
6312
|
+
const body = await readJson(request);
|
|
6313
|
+
if (typeof body.id !== "string" || body.id === "") return errorResponse("id is required");
|
|
6314
|
+
if (body.tags !== void 0 && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== "string"))) {
|
|
6315
|
+
return errorResponse("tags must be a string array");
|
|
6316
|
+
}
|
|
6317
|
+
if (body.relations !== void 0 && (!Array.isArray(body.relations) || body.relations.some(
|
|
6318
|
+
(relation) => typeof relation !== "object" || relation === null || typeof relation.type !== "string" || typeof relation.target !== "string"
|
|
6319
|
+
))) {
|
|
6320
|
+
return errorResponse("relations must be {type,target} objects");
|
|
6321
|
+
}
|
|
6322
|
+
try {
|
|
6323
|
+
const doc = deps.store.updateMeta(body.id, {
|
|
6324
|
+
...body.tags === void 0 ? {} : { tags: body.tags },
|
|
6325
|
+
...body.relations === void 0 ? {} : { relations: body.relations }
|
|
6326
|
+
});
|
|
6327
|
+
return json({ ok: true, id: doc.id, project: doc.project ?? null, tags: doc.tags ?? [], relations: doc.relations ?? [] });
|
|
6328
|
+
} catch (error) {
|
|
6329
|
+
return errorResponse(error instanceof Error ? error.message : String(error), 409);
|
|
6330
|
+
}
|
|
6331
|
+
}
|
|
6332
|
+
};
|
|
5015
6333
|
const item = {
|
|
5016
6334
|
path: "/api/agent-memory/item",
|
|
5017
6335
|
methods: ["GET"],
|
|
@@ -5033,7 +6351,7 @@ function guiRoutes(deps) {
|
|
|
5033
6351
|
fetch: async (request) => {
|
|
5034
6352
|
const query = (new URL(request.url).searchParams.get("q") ?? "").trim();
|
|
5035
6353
|
if (query === "") return errorResponse("q is required");
|
|
5036
|
-
return json(buildSearch(deps.store, query));
|
|
6354
|
+
return json(await buildSearch(deps.store, query));
|
|
5037
6355
|
}
|
|
5038
6356
|
};
|
|
5039
6357
|
const exportRoute = {
|
|
@@ -5209,6 +6527,10 @@ function guiRoutes(deps) {
|
|
|
5209
6527
|
return [
|
|
5210
6528
|
status,
|
|
5211
6529
|
list,
|
|
6530
|
+
graph,
|
|
6531
|
+
projects,
|
|
6532
|
+
moveProject,
|
|
6533
|
+
meta,
|
|
5212
6534
|
item,
|
|
5213
6535
|
search2,
|
|
5214
6536
|
exportRoute,
|
|
@@ -5292,8 +6614,8 @@ function openHostService(home, session, options = {}) {
|
|
|
5292
6614
|
mode: runtime.config.personalization.mode,
|
|
5293
6615
|
session,
|
|
5294
6616
|
config: runtime.config,
|
|
5295
|
-
recall(input) {
|
|
5296
|
-
return store.
|
|
6617
|
+
async recall(input) {
|
|
6618
|
+
return store.recallAsync(input.query, {
|
|
5297
6619
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
5298
6620
|
...input.kind === void 0 ? {} : { kinds: [input.kind] }
|
|
5299
6621
|
});
|
|
@@ -5324,6 +6646,8 @@ function openHostService(home, session, options = {}) {
|
|
|
5324
6646
|
body: raw.body,
|
|
5325
6647
|
evidence_level: "observed",
|
|
5326
6648
|
...raw.valid_from === void 0 ? {} : { valid_from: raw.valid_from },
|
|
6649
|
+
...raw.project === void 0 ? {} : { project: raw.project },
|
|
6650
|
+
...raw.tags === void 0 ? {} : { tags: raw.tags },
|
|
5327
6651
|
...raw.error_signatures === void 0 ? {} : { error_signatures: raw.error_signatures }
|
|
5328
6652
|
},
|
|
5329
6653
|
{ author: "agent", provenance }
|
|
@@ -5372,6 +6696,8 @@ function openHostService(home, session, options = {}) {
|
|
|
5372
6696
|
body: replacement.body,
|
|
5373
6697
|
evidence_level: replacement.evidence_level ?? "observed",
|
|
5374
6698
|
...replacement.valid_from === void 0 ? {} : { valid_from: replacement.valid_from },
|
|
6699
|
+
...replacement.project === void 0 ? {} : { project: replacement.project },
|
|
6700
|
+
...replacement.tags === void 0 ? {} : { tags: replacement.tags },
|
|
5375
6701
|
...replacement.error_signatures === void 0 ? {} : { error_signatures: replacement.error_signatures },
|
|
5376
6702
|
provenance
|
|
5377
6703
|
});
|