@agent_memory/dsh 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/client.js +500 -16
- package/dist/plugin.mjs +836 -95
- 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"),
|
|
@@ -553,7 +554,9 @@ var configSchema = {
|
|
|
553
554
|
recall: {
|
|
554
555
|
limit: { type: "int", default: 5, min: 1, max: 100 },
|
|
555
556
|
min_score: { type: "int", default: 500, min: 0, max: 1e3 },
|
|
556
|
-
max_limit: { type: "int", default: 20, min: 1, max: 100 }
|
|
557
|
+
max_limit: { type: "int", default: 20, min: 1, max: 100 },
|
|
558
|
+
hybrid: { type: "bool", default: false },
|
|
559
|
+
embedding_weight: { type: "int", default: 400, min: 0, max: 1e3 }
|
|
557
560
|
},
|
|
558
561
|
distill: {
|
|
559
562
|
max_proposals: { type: "int", default: 5, min: 1, max: 5 },
|
|
@@ -718,6 +721,79 @@ function ensureDir(dir) {
|
|
|
718
721
|
|
|
719
722
|
// packages/core/src/model/frontmatter.ts
|
|
720
723
|
import { createHash } from "node:crypto";
|
|
724
|
+
|
|
725
|
+
// packages/core/src/model/metadata.ts
|
|
726
|
+
var PROJECT_NAME_MAX = 64;
|
|
727
|
+
var TAG_MAX = 32;
|
|
728
|
+
var TAGS_MAX = 8;
|
|
729
|
+
var RELATIONS_MAX = 16;
|
|
730
|
+
var RELATION_TYPES = ["relates_to", "derived_from"];
|
|
731
|
+
var MEMORY_ID = /^mem-\d{8}-[0-9a-f]{8}$/;
|
|
732
|
+
function projectProblem(value) {
|
|
733
|
+
if (value === null || value === void 0) return void 0;
|
|
734
|
+
if (typeof value !== "string") return "project must be a string or null";
|
|
735
|
+
const name2 = value.trim();
|
|
736
|
+
if (name2 === "") return "project must not be empty";
|
|
737
|
+
if (name2 !== value) return "project must not have leading or trailing whitespace";
|
|
738
|
+
if ([...name2].length > PROJECT_NAME_MAX) {
|
|
739
|
+
return `project must be at most ${PROJECT_NAME_MAX} characters`;
|
|
740
|
+
}
|
|
741
|
+
if (/[\r\n]/.test(name2)) return "project must be a single line";
|
|
742
|
+
if (/[\\/]/.test(name2)) return "project must not contain path separators";
|
|
743
|
+
if (name2 === "\u672A\u5F52\u7C7B") return 'project name "\u672A\u5F52\u7C7B" is reserved for the default bucket';
|
|
744
|
+
return void 0;
|
|
745
|
+
}
|
|
746
|
+
function tagsProblem(value) {
|
|
747
|
+
if (!Array.isArray(value)) return "tags must be an array";
|
|
748
|
+
if (value.length > TAGS_MAX) return `tags allows at most ${TAGS_MAX} entries`;
|
|
749
|
+
const seen = /* @__PURE__ */ new Set();
|
|
750
|
+
for (const entry of value) {
|
|
751
|
+
if (typeof entry !== "string") return "each tag must be a string";
|
|
752
|
+
if (entry.length === 0) return "tags must not contain empty entries";
|
|
753
|
+
if ([...entry].length > TAG_MAX) return `each tag must be at most ${TAG_MAX} characters`;
|
|
754
|
+
if (/[\r\n]/.test(entry)) return "tags must be single-line";
|
|
755
|
+
if (seen.has(entry)) return `duplicate tag "${entry}"`;
|
|
756
|
+
seen.add(entry);
|
|
757
|
+
}
|
|
758
|
+
return void 0;
|
|
759
|
+
}
|
|
760
|
+
function relationsProblem(value, selfId) {
|
|
761
|
+
if (!Array.isArray(value)) return "relations must be an array";
|
|
762
|
+
if (value.length > RELATIONS_MAX) return `relations allows at most ${RELATIONS_MAX} entries`;
|
|
763
|
+
const seen = /* @__PURE__ */ new Set();
|
|
764
|
+
for (const entry of value) {
|
|
765
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
766
|
+
return "each relation must be an object";
|
|
767
|
+
}
|
|
768
|
+
const relation = entry;
|
|
769
|
+
if (typeof relation.type !== "string" || !RELATION_TYPES.includes(relation.type)) {
|
|
770
|
+
return "relation.type must be relates_to or derived_from";
|
|
771
|
+
}
|
|
772
|
+
if (typeof relation.target !== "string" || !MEMORY_ID.test(relation.target)) {
|
|
773
|
+
return "relation.target must look like mem-YYYYMMDD-8hex";
|
|
774
|
+
}
|
|
775
|
+
if (relation.target === selfId) return "a relation must not target itself";
|
|
776
|
+
const key = `${relation.type}:${relation.target}`;
|
|
777
|
+
if (seen.has(key)) return "relations must not contain duplicates";
|
|
778
|
+
seen.add(key);
|
|
779
|
+
}
|
|
780
|
+
return void 0;
|
|
781
|
+
}
|
|
782
|
+
function sourceProblem(value) {
|
|
783
|
+
if (value === null || value === void 0) return void 0;
|
|
784
|
+
if (typeof value !== "object" || Array.isArray(value)) return "source must be an object or null";
|
|
785
|
+
const source = value;
|
|
786
|
+
if (typeof source.type !== "string" || source.type.length === 0 || [...source.type].length > 32) {
|
|
787
|
+
return "source.type must be a non-empty string up to 32 characters";
|
|
788
|
+
}
|
|
789
|
+
if (typeof source.id !== "string" || source.id.length === 0 || [...source.id].length > 128) {
|
|
790
|
+
return "source.id must be a non-empty string up to 128 characters";
|
|
791
|
+
}
|
|
792
|
+
if (/[\r\n]/.test(source.type) || /[\r\n]/.test(source.id)) return "source values must be single-line";
|
|
793
|
+
return void 0;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// packages/core/src/model/frontmatter.ts
|
|
721
797
|
var FrontmatterError = class extends Error {
|
|
722
798
|
field;
|
|
723
799
|
filePath;
|
|
@@ -733,6 +809,10 @@ var FIELD_ORDER = [
|
|
|
733
809
|
"mem_schema",
|
|
734
810
|
"kind",
|
|
735
811
|
"scope",
|
|
812
|
+
"project",
|
|
813
|
+
"tags",
|
|
814
|
+
"relations",
|
|
815
|
+
"source",
|
|
736
816
|
"abstract",
|
|
737
817
|
"created_at",
|
|
738
818
|
"valid_from",
|
|
@@ -805,10 +885,32 @@ function parseMemoryFile(text, filePath, limits) {
|
|
|
805
885
|
}
|
|
806
886
|
const id = requireString(values.get("id"), "id", filePath);
|
|
807
887
|
if (!ID.test(id)) fail("id", "must look like mem-YYYYMMDD-8hex", filePath);
|
|
808
|
-
if (values.get("mem_schema") !== 1
|
|
888
|
+
if (values.get("mem_schema") !== 1 && values.get("mem_schema") !== 2) {
|
|
889
|
+
fail("mem_schema", "must be 1 or 2", filePath);
|
|
890
|
+
}
|
|
891
|
+
const memSchema = values.get("mem_schema");
|
|
809
892
|
const kind = requireString(values.get("kind"), "kind", filePath);
|
|
810
893
|
if (!KINDS.has(kind)) fail("kind", "must be fact, pitfall or exp", filePath);
|
|
811
894
|
if (values.get("scope") !== "local") fail("scope", 'must be "local" in V1', filePath);
|
|
895
|
+
const rawProject = values.has("project") ? values.get("project") : null;
|
|
896
|
+
const projectIssue = projectProblem(rawProject);
|
|
897
|
+
if (projectIssue !== void 0) fail("project", projectIssue, filePath);
|
|
898
|
+
const project = rawProject === null || rawProject === void 0 ? null : rawProject;
|
|
899
|
+
const rawTags = values.has("tags") ? values.get("tags") : [];
|
|
900
|
+
const tagsIssue = tagsProblem(rawTags);
|
|
901
|
+
if (tagsIssue !== void 0) fail("tags", tagsIssue, filePath);
|
|
902
|
+
const tags = requireArray(rawTags, "tags", filePath).map((entry) => entry);
|
|
903
|
+
const rawRelations = values.has("relations") ? values.get("relations") : [];
|
|
904
|
+
const relationsIssue = relationsProblem(rawRelations, id);
|
|
905
|
+
if (relationsIssue !== void 0) fail("relations", relationsIssue, filePath);
|
|
906
|
+
const relations = requireArray(rawRelations, "relations", filePath).map((entry) => {
|
|
907
|
+
const relation = entry;
|
|
908
|
+
return { type: relation.type, target: relation.target };
|
|
909
|
+
});
|
|
910
|
+
const rawSource = values.has("source") ? values.get("source") : null;
|
|
911
|
+
const sourceIssue = sourceProblem(rawSource);
|
|
912
|
+
if (sourceIssue !== void 0) fail("source", sourceIssue, filePath);
|
|
913
|
+
const source = rawSource === null || rawSource === void 0 ? null : rawSource;
|
|
812
914
|
const abstract = requireString(values.get("abstract"), "abstract", filePath);
|
|
813
915
|
if (abstract.trim() === "") fail("abstract", "must not be empty", filePath);
|
|
814
916
|
if (/[\r\n]/.test(abstract)) fail("abstract", "must be a single line", filePath);
|
|
@@ -881,9 +983,13 @@ function parseMemoryFile(text, filePath, limits) {
|
|
|
881
983
|
}
|
|
882
984
|
return {
|
|
883
985
|
id,
|
|
884
|
-
mem_schema:
|
|
986
|
+
mem_schema: memSchema,
|
|
885
987
|
kind,
|
|
886
988
|
scope: "local",
|
|
989
|
+
project,
|
|
990
|
+
tags,
|
|
991
|
+
relations,
|
|
992
|
+
source,
|
|
887
993
|
abstract,
|
|
888
994
|
created_at: createdAt,
|
|
889
995
|
valid_from: validFrom,
|
|
@@ -904,6 +1010,10 @@ function serializeMemory(doc) {
|
|
|
904
1010
|
mem_schema: doc.mem_schema,
|
|
905
1011
|
kind: doc.kind,
|
|
906
1012
|
scope: doc.scope,
|
|
1013
|
+
project: doc.project ?? null,
|
|
1014
|
+
tags: doc.tags ?? [],
|
|
1015
|
+
relations: doc.relations ?? [],
|
|
1016
|
+
source: doc.source ?? null,
|
|
907
1017
|
abstract: doc.abstract,
|
|
908
1018
|
created_at: doc.created_at,
|
|
909
1019
|
valid_from: doc.valid_from,
|
|
@@ -1228,9 +1338,99 @@ function missingContentWarnings(paths) {
|
|
|
1228
1338
|
return expected.filter(([filePath]) => !existsSync2(filePath)).map(([, name2]) => `${name2} is not present yet; that injection section will be empty`);
|
|
1229
1339
|
}
|
|
1230
1340
|
|
|
1341
|
+
// packages/core/src/projects/projects.ts
|
|
1342
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1343
|
+
var ProjectError = class extends Error {
|
|
1344
|
+
code;
|
|
1345
|
+
constructor(code, message) {
|
|
1346
|
+
super(message);
|
|
1347
|
+
this.name = "ProjectError";
|
|
1348
|
+
this.code = code;
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
var DESCRIPTION_MAX = 280;
|
|
1352
|
+
var TIMESTAMP2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
1353
|
+
function readRegistry(home) {
|
|
1354
|
+
const file = memoryPaths(home).projectsFile;
|
|
1355
|
+
if (!existsSync3(file)) return { schema: 1, projects: [] };
|
|
1356
|
+
let parsed;
|
|
1357
|
+
try {
|
|
1358
|
+
parsed = JSON.parse(readFileSync3(file, "utf8").replace(/^\uFEFF/, ""));
|
|
1359
|
+
} catch {
|
|
1360
|
+
throw new ProjectError("INVALID", `projects.json is not valid JSON: ${file}`);
|
|
1361
|
+
}
|
|
1362
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1363
|
+
throw new ProjectError("INVALID", "projects.json must be an object");
|
|
1364
|
+
}
|
|
1365
|
+
const record = parsed;
|
|
1366
|
+
if (record.schema !== 1) throw new ProjectError("INVALID", "projects.json schema must be 1");
|
|
1367
|
+
if (!Array.isArray(record.projects)) throw new ProjectError("INVALID", "projects.json projects must be an array");
|
|
1368
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1369
|
+
const projects = [];
|
|
1370
|
+
for (const raw of record.projects) {
|
|
1371
|
+
if (typeof raw !== "object" || raw === null) throw new ProjectError("INVALID", "each project must be an object");
|
|
1372
|
+
const entry = raw;
|
|
1373
|
+
const issue = projectProblem(entry.name);
|
|
1374
|
+
if (issue !== void 0) throw new ProjectError("INVALID", issue);
|
|
1375
|
+
if (typeof entry.description !== "string" || [...entry.description].length > DESCRIPTION_MAX) {
|
|
1376
|
+
throw new ProjectError("INVALID", `project description must be a string up to ${DESCRIPTION_MAX} characters`);
|
|
1377
|
+
}
|
|
1378
|
+
if (typeof entry.created_at !== "string" || !TIMESTAMP2.test(entry.created_at)) {
|
|
1379
|
+
throw new ProjectError("INVALID", "project created_at must be an ISO timestamp");
|
|
1380
|
+
}
|
|
1381
|
+
if (typeof entry.updated_at !== "string" || !TIMESTAMP2.test(entry.updated_at)) {
|
|
1382
|
+
throw new ProjectError("INVALID", "project updated_at must be an ISO timestamp");
|
|
1383
|
+
}
|
|
1384
|
+
const name2 = entry.name;
|
|
1385
|
+
if (seen.has(name2.toLocaleLowerCase())) throw new ProjectError("INVALID", `duplicate project name: ${name2}`);
|
|
1386
|
+
seen.add(name2.toLocaleLowerCase());
|
|
1387
|
+
projects.push({
|
|
1388
|
+
name: name2,
|
|
1389
|
+
description: entry.description,
|
|
1390
|
+
created_at: entry.created_at,
|
|
1391
|
+
updated_at: entry.updated_at
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
return { schema: 1, projects };
|
|
1395
|
+
}
|
|
1396
|
+
function writeRegistry(home, registry) {
|
|
1397
|
+
writeFileAtomic(memoryPaths(home).projectsFile, `${JSON.stringify(registry, null, 2)}
|
|
1398
|
+
`);
|
|
1399
|
+
}
|
|
1400
|
+
function listProjects(home) {
|
|
1401
|
+
return readRegistry(home).projects;
|
|
1402
|
+
}
|
|
1403
|
+
function createProject(options) {
|
|
1404
|
+
const name2 = options.name.trim();
|
|
1405
|
+
const issue = projectProblem(name2);
|
|
1406
|
+
if (issue !== void 0) throw new ProjectError("INVALID", issue);
|
|
1407
|
+
if (name2.length > PROJECT_NAME_MAX) throw new ProjectError("INVALID", "project name is too long");
|
|
1408
|
+
const description = (options.description ?? "").trim();
|
|
1409
|
+
if ([...description].length > DESCRIPTION_MAX) {
|
|
1410
|
+
throw new ProjectError("INVALID", `project description must be at most ${DESCRIPTION_MAX} characters`);
|
|
1411
|
+
}
|
|
1412
|
+
if (/[\r\n]/.test(description)) throw new ProjectError("INVALID", "project description must be a single line");
|
|
1413
|
+
const registry = readRegistry(options.home);
|
|
1414
|
+
if (registry.projects.some((project2) => project2.name.toLocaleLowerCase() === name2.toLocaleLowerCase())) {
|
|
1415
|
+
throw new ProjectError("EXISTS", `project already exists: ${name2}`);
|
|
1416
|
+
}
|
|
1417
|
+
const at = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1418
|
+
const project = { name: name2, description, created_at: at, updated_at: at };
|
|
1419
|
+
writeRegistry(options.home, { schema: 1, projects: [...registry.projects, project] });
|
|
1420
|
+
return project;
|
|
1421
|
+
}
|
|
1422
|
+
function deleteProject(home, name2) {
|
|
1423
|
+
const registry = readRegistry(home);
|
|
1424
|
+
const lower = name2.toLocaleLowerCase();
|
|
1425
|
+
const kept = registry.projects.filter((project) => project.name.toLocaleLowerCase() !== lower);
|
|
1426
|
+
if (kept.length === registry.projects.length) return false;
|
|
1427
|
+
writeRegistry(home, { schema: 1, projects: kept });
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1231
1431
|
// packages/core/src/fs/lock.ts
|
|
1232
1432
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1233
|
-
import { closeSync as closeSync2, fsyncSync as fsyncSync2, openSync as openSync2, readFileSync as
|
|
1433
|
+
import { closeSync as closeSync2, fsyncSync as fsyncSync2, openSync as openSync2, readFileSync as readFileSync4, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync2, writeSync } from "node:fs";
|
|
1234
1434
|
var LockError = class extends Error {
|
|
1235
1435
|
code;
|
|
1236
1436
|
constructor(code, message) {
|
|
@@ -1241,7 +1441,7 @@ var LockError = class extends Error {
|
|
|
1241
1441
|
};
|
|
1242
1442
|
function readLock(file) {
|
|
1243
1443
|
try {
|
|
1244
|
-
const raw = JSON.parse(
|
|
1444
|
+
const raw = JSON.parse(readFileSync4(file, "utf8"));
|
|
1245
1445
|
if (typeof raw.pid === "number" && typeof raw.token === "string" && typeof raw.acquired_at === "number" && typeof raw.heartbeat_at === "number") {
|
|
1246
1446
|
return { pid: raw.pid, token: raw.token, acquired_at: raw.acquired_at, heartbeat_at: raw.heartbeat_at };
|
|
1247
1447
|
}
|
|
@@ -1462,7 +1662,7 @@ function supersedeProblems(candidate, docs) {
|
|
|
1462
1662
|
|
|
1463
1663
|
// packages/core/src/journal/journal.ts
|
|
1464
1664
|
import { createHash as createHash3 } from "node:crypto";
|
|
1465
|
-
import { closeSync as closeSync3, fsyncSync as fsyncSync3, mkdirSync as mkdirSync2, openSync as openSync3, readFileSync as
|
|
1665
|
+
import { closeSync as closeSync3, fsyncSync as fsyncSync3, mkdirSync as mkdirSync2, openSync as openSync3, readFileSync as readFileSync5, readdirSync, statSync as statSync3, writeSync as writeSync2 } from "node:fs";
|
|
1466
1666
|
import { join as join3 } from "node:path";
|
|
1467
1667
|
var SEGMENT_BYTES = 5 * 1024 * 1024;
|
|
1468
1668
|
var ZERO_HASH = "0".repeat(64);
|
|
@@ -1486,7 +1686,7 @@ var Journal = class {
|
|
|
1486
1686
|
const segments = this.segments();
|
|
1487
1687
|
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
1488
1688
|
const file = join3(this.dir, segments[index]);
|
|
1489
|
-
const lines =
|
|
1689
|
+
const lines = readFileSync5(file, "utf8").split("\n").filter((line) => line !== "");
|
|
1490
1690
|
if (lines.length === 0) continue;
|
|
1491
1691
|
const last = lines[lines.length - 1];
|
|
1492
1692
|
try {
|
|
@@ -1509,7 +1709,7 @@ var Journal = class {
|
|
|
1509
1709
|
const file = join3(this.dir, name2);
|
|
1510
1710
|
let text;
|
|
1511
1711
|
try {
|
|
1512
|
-
text =
|
|
1712
|
+
text = readFileSync5(file, "utf8");
|
|
1513
1713
|
} catch (error) {
|
|
1514
1714
|
issues.push({ kind: "parse", message: `cannot read ${name2}: ${String(error)}` });
|
|
1515
1715
|
continue;
|
|
@@ -1605,8 +1805,8 @@ var Journal = class {
|
|
|
1605
1805
|
};
|
|
1606
1806
|
|
|
1607
1807
|
// packages/core/src/store/store.ts
|
|
1608
|
-
import { existsSync as
|
|
1609
|
-
import { join as
|
|
1808
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync7, rmSync as rmSync3 } from "node:fs";
|
|
1809
|
+
import { join as join5 } from "node:path";
|
|
1610
1810
|
|
|
1611
1811
|
// packages/core/src/index/tokenizer.ts
|
|
1612
1812
|
var TOKENIZER_VERSION = 1;
|
|
@@ -1762,6 +1962,116 @@ function search(index, query, options) {
|
|
|
1762
1962
|
}).slice(0, options.limit).map((entry) => ({ id: entry.doc.id, score: Number(entry.normalized.toFixed(6)) }));
|
|
1763
1963
|
}
|
|
1764
1964
|
|
|
1965
|
+
// packages/core/src/index/embedding.ts
|
|
1966
|
+
var DIMENSIONS = 256;
|
|
1967
|
+
function hashFeature(feature) {
|
|
1968
|
+
let hash = 2166136261;
|
|
1969
|
+
for (let index = 0; index < feature.length; index += 1) {
|
|
1970
|
+
hash ^= feature.charCodeAt(index);
|
|
1971
|
+
hash = Math.imul(hash, 16777619);
|
|
1972
|
+
}
|
|
1973
|
+
return hash >>> 0;
|
|
1974
|
+
}
|
|
1975
|
+
var HashEmbeddingProvider = class {
|
|
1976
|
+
name = "hash-256";
|
|
1977
|
+
dimensions = DIMENSIONS;
|
|
1978
|
+
embed(text) {
|
|
1979
|
+
const vector = new Float32Array(DIMENSIONS);
|
|
1980
|
+
const tokens = tokenize(text);
|
|
1981
|
+
const features = [...tokens, ...tokens.map((token) => `#${token}`)];
|
|
1982
|
+
for (const feature of features) {
|
|
1983
|
+
const bucket = hashFeature(feature) % DIMENSIONS;
|
|
1984
|
+
vector[bucket] = (vector[bucket] ?? 0) + 1;
|
|
1985
|
+
}
|
|
1986
|
+
let norm = 0;
|
|
1987
|
+
for (const value of vector) norm += value * value;
|
|
1988
|
+
norm = Math.sqrt(norm);
|
|
1989
|
+
if (norm > 0) for (let index = 0; index < vector.length; index += 1) vector[index] = (vector[index] ?? 0) / norm;
|
|
1990
|
+
return vector;
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
function cosine(left, right) {
|
|
1994
|
+
if (left.length !== right.length) throw new Error("embedding dimensions differ");
|
|
1995
|
+
let dot = 0;
|
|
1996
|
+
for (let index = 0; index < left.length; index += 1) dot += (left[index] ?? 0) * (right[index] ?? 0);
|
|
1997
|
+
return Math.max(0, Math.min(1, dot));
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// packages/core/src/index/hybrid.ts
|
|
2001
|
+
function fuseScores(lexical, vector, embeddingWeightPermille) {
|
|
2002
|
+
const weight = Math.min(1e3, Math.max(0, embeddingWeightPermille)) / 1e3;
|
|
2003
|
+
const ids = /* @__PURE__ */ new Set([...lexical.keys(), ...vector.keys()]);
|
|
2004
|
+
const fused = /* @__PURE__ */ new Map();
|
|
2005
|
+
for (const id of ids) {
|
|
2006
|
+
const lexicalScore = lexical.get(id) ?? 0;
|
|
2007
|
+
const vectorScore = vector.get(id) ?? 0;
|
|
2008
|
+
fused.set(id, (1 - weight) * lexicalScore + weight * vectorScore);
|
|
2009
|
+
}
|
|
2010
|
+
return fused;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// packages/core/src/embed/cache.ts
|
|
2014
|
+
import { mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2015
|
+
import { tmpdir } from "node:os";
|
|
2016
|
+
import { join as join4 } from "node:path";
|
|
2017
|
+
import { spawn } from "node:child_process";
|
|
2018
|
+
var CACHE_SCHEMA = 1;
|
|
2019
|
+
function embeddingsFile(home) {
|
|
2020
|
+
return join4(memoryPaths(home).indexDir, "embeddings.json");
|
|
2021
|
+
}
|
|
2022
|
+
function readEmbeddingCache(home) {
|
|
2023
|
+
try {
|
|
2024
|
+
const parsed = JSON.parse(readFileSync6(embeddingsFile(home), "utf8"));
|
|
2025
|
+
if (parsed.schema !== CACHE_SCHEMA) return void 0;
|
|
2026
|
+
if (typeof parsed.provider !== "string" || typeof parsed.dimensions !== "number") return void 0;
|
|
2027
|
+
if (typeof parsed.generated_at !== "string" || typeof parsed.docs !== "object" || parsed.docs === null) {
|
|
2028
|
+
return void 0;
|
|
2029
|
+
}
|
|
2030
|
+
return parsed;
|
|
2031
|
+
} catch {
|
|
2032
|
+
return void 0;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
function vectorizerCommand() {
|
|
2036
|
+
const command = process.env.AGENT_MEMORY_VECTORIZER?.trim();
|
|
2037
|
+
return command === void 0 || command === "" ? void 0 : command;
|
|
2038
|
+
}
|
|
2039
|
+
async function embedTexts(command, texts) {
|
|
2040
|
+
const dir = mkdtempSync(join4(tmpdir(), "agent-memory-embed-"));
|
|
2041
|
+
const inputPath = join4(dir, "input.json");
|
|
2042
|
+
const outputPath = join4(dir, "output.json");
|
|
2043
|
+
try {
|
|
2044
|
+
writeFileSync3(inputPath, JSON.stringify({ texts }));
|
|
2045
|
+
await new Promise((resolve3, reject) => {
|
|
2046
|
+
const child = spawn(command, [inputPath, outputPath], {
|
|
2047
|
+
stdio: "ignore",
|
|
2048
|
+
shell: true,
|
|
2049
|
+
windowsHide: true
|
|
2050
|
+
});
|
|
2051
|
+
child.once("error", reject);
|
|
2052
|
+
child.once("exit", (code) => {
|
|
2053
|
+
if (code === 0) resolve3();
|
|
2054
|
+
else reject(new Error(`vectorizer exited with ${code ?? "no code"}`));
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2057
|
+
const parsed = JSON.parse(readFileSync6(outputPath, "utf8"));
|
|
2058
|
+
if (!Array.isArray(parsed.vectors) || parsed.vectors.length !== texts.length) {
|
|
2059
|
+
throw new Error("vectorizer output must contain one vector per text");
|
|
2060
|
+
}
|
|
2061
|
+
let dimensions = 0;
|
|
2062
|
+
for (const vector of parsed.vectors) {
|
|
2063
|
+
if (!Array.isArray(vector) || vector.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
|
|
2064
|
+
throw new Error("vectorizer vectors must be arrays of finite numbers");
|
|
2065
|
+
}
|
|
2066
|
+
if (dimensions === 0) dimensions = vector.length;
|
|
2067
|
+
if (vector.length !== dimensions) throw new Error("vectorizer vectors have inconsistent dimensions");
|
|
2068
|
+
}
|
|
2069
|
+
return { vectors: parsed.vectors, dimensions };
|
|
2070
|
+
} finally {
|
|
2071
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
1765
2075
|
// packages/core/src/scan/scan.ts
|
|
1766
2076
|
var ScanError = class extends Error {
|
|
1767
2077
|
findings;
|
|
@@ -1874,7 +2184,7 @@ var MemoryStore = class {
|
|
|
1874
2184
|
if (value.hash !== "") this.accounted.set(id, value.hash);
|
|
1875
2185
|
}
|
|
1876
2186
|
for (const kind of KINDS2) {
|
|
1877
|
-
const dir =
|
|
2187
|
+
const dir = join5(this.paths.memoriesDir, kind);
|
|
1878
2188
|
let names;
|
|
1879
2189
|
try {
|
|
1880
2190
|
names = readdirSync2(dir).filter((name2) => name2.endsWith(".md"));
|
|
@@ -1882,10 +2192,10 @@ var MemoryStore = class {
|
|
|
1882
2192
|
continue;
|
|
1883
2193
|
}
|
|
1884
2194
|
for (const name2 of names) {
|
|
1885
|
-
const file =
|
|
2195
|
+
const file = join5(dir, name2);
|
|
1886
2196
|
const id = name2.slice(0, -3);
|
|
1887
2197
|
try {
|
|
1888
|
-
const text =
|
|
2198
|
+
const text = readFileSync7(file, "utf8");
|
|
1889
2199
|
const doc = parseMemoryFile(text, file, {
|
|
1890
2200
|
abstractChars: this.config.limits.abstract_chars,
|
|
1891
2201
|
bodyChars: this.config.limits.body_chars,
|
|
@@ -1921,7 +2231,7 @@ var MemoryStore = class {
|
|
|
1921
2231
|
}
|
|
1922
2232
|
}
|
|
1923
2233
|
try {
|
|
1924
|
-
const manifest = JSON.parse(
|
|
2234
|
+
const manifest = JSON.parse(readFileSync7(this.paths.manifestFile, "utf8"));
|
|
1925
2235
|
const expected = manifest.journal_head;
|
|
1926
2236
|
const expectedSeq = expected?.seq;
|
|
1927
2237
|
const expectedHash = expected?.hash;
|
|
@@ -1971,14 +2281,43 @@ var MemoryStore = class {
|
|
|
1971
2281
|
(doc) => !this.deleted.has(doc.id) && !this.quarantined.has(doc.id)
|
|
1972
2282
|
);
|
|
1973
2283
|
const index = buildIndex(activeSet, TOKENIZER_VERSION);
|
|
1974
|
-
const found = search(index, text, {
|
|
1975
|
-
limit,
|
|
1976
|
-
minScorePermille: this.config.recall.min_score,
|
|
1977
|
-
...options.kinds === void 0 ? {} : { kinds: options.kinds }
|
|
1978
|
-
});
|
|
1979
2284
|
const byId = new Map(activeSet.map((doc) => [doc.id, doc]));
|
|
2285
|
+
const minScore = this.config.recall.min_score / 1e3;
|
|
2286
|
+
let ranked;
|
|
2287
|
+
if (this.config.recall.hybrid) {
|
|
2288
|
+
const lexicalHits = search(index, text, {
|
|
2289
|
+
limit: activeSet.length,
|
|
2290
|
+
minScorePermille: 0,
|
|
2291
|
+
...options.kinds === void 0 ? {} : { kinds: options.kinds }
|
|
2292
|
+
});
|
|
2293
|
+
const lexical = new Map(lexicalHits.map((hit) => [hit.id, hit.score]));
|
|
2294
|
+
const provider = new HashEmbeddingProvider();
|
|
2295
|
+
const queryVector = provider.embed(text);
|
|
2296
|
+
const vector = /* @__PURE__ */ new Map();
|
|
2297
|
+
const kindFilter = options.kinds === void 0 ? void 0 : new Set(options.kinds);
|
|
2298
|
+
for (const doc of activeSet) {
|
|
2299
|
+
if (kindFilter !== void 0 && !kindFilter.has(doc.kind)) continue;
|
|
2300
|
+
vector.set(doc.id, cosine(provider.embed(`${doc.abstract}
|
|
2301
|
+
${doc.body}`), queryVector));
|
|
2302
|
+
}
|
|
2303
|
+
ranked = [...fuseScores(lexical, vector, this.config.recall.embedding_weight).entries()].filter(([, score]) => score >= minScore).map(([id, score]) => ({ id, score })).sort((left, right) => {
|
|
2304
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
2305
|
+
const leftDoc = byId.get(left.id);
|
|
2306
|
+
const rightDoc = byId.get(right.id);
|
|
2307
|
+
if (leftDoc !== void 0 && rightDoc !== void 0 && rightDoc.valid_from !== leftDoc.valid_from) {
|
|
2308
|
+
return rightDoc.valid_from.localeCompare(leftDoc.valid_from);
|
|
2309
|
+
}
|
|
2310
|
+
return left.id.localeCompare(right.id);
|
|
2311
|
+
}).slice(0, limit);
|
|
2312
|
+
} else {
|
|
2313
|
+
ranked = search(index, text, {
|
|
2314
|
+
limit,
|
|
2315
|
+
minScorePermille: this.config.recall.min_score,
|
|
2316
|
+
...options.kinds === void 0 ? {} : { kinds: options.kinds }
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
1980
2319
|
const hits = [];
|
|
1981
|
-
for (const hit of
|
|
2320
|
+
for (const hit of ranked) {
|
|
1982
2321
|
const doc = byId.get(hit.id);
|
|
1983
2322
|
if (doc === void 0) continue;
|
|
1984
2323
|
hits.push({
|
|
@@ -1996,6 +2335,214 @@ var MemoryStore = class {
|
|
|
1996
2335
|
warnings: this.quarantined.size === 0 ? [] : [...this.quarantined.values()].slice(0, 5)
|
|
1997
2336
|
};
|
|
1998
2337
|
}
|
|
2338
|
+
/**
|
|
2339
|
+
* Hybrid retrieval entry point. Sync `recall` stays BM25/hash-only so the
|
|
2340
|
+
* CLI/tests keep a cheap path; the host calls this async version when the
|
|
2341
|
+
* sidecar vector cache exists.
|
|
2342
|
+
*/
|
|
2343
|
+
async recallAsync(query, options = {}) {
|
|
2344
|
+
const text = query.trim();
|
|
2345
|
+
if (text === "" || !this.config.recall.hybrid) return this.recall(query, options);
|
|
2346
|
+
const cache = readEmbeddingCache(this.paths.home);
|
|
2347
|
+
const command = vectorizerCommand();
|
|
2348
|
+
if (cache === void 0 || command === void 0) {
|
|
2349
|
+
const fallback = this.recall(query, options);
|
|
2350
|
+
return {
|
|
2351
|
+
...fallback,
|
|
2352
|
+
warnings: [
|
|
2353
|
+
...fallback.warnings,
|
|
2354
|
+
cache === void 0 ? "hybrid requested but .index/embeddings.json is missing; run `mem embed --apply`" : "hybrid requested but AGENT_MEMORY_VECTORIZER is not set"
|
|
2355
|
+
]
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
try {
|
|
2359
|
+
const queryVector = (await embedTexts(command, [text])).vectors[0];
|
|
2360
|
+
if (queryVector === void 0 || queryVector.length !== cache.dimensions) {
|
|
2361
|
+
throw new Error(`query vector does not match cache dimensions (${cache.dimensions})`);
|
|
2362
|
+
}
|
|
2363
|
+
this.reload();
|
|
2364
|
+
const date = options.asOf ?? today(this.now());
|
|
2365
|
+
const limit = Math.min(options.limit ?? this.config.recall.limit, this.config.recall.max_limit);
|
|
2366
|
+
const activeSet = activeDocs(this.docs.values(), date).filter(
|
|
2367
|
+
(doc) => !this.deleted.has(doc.id) && !this.quarantined.has(doc.id)
|
|
2368
|
+
);
|
|
2369
|
+
const index = buildIndex(activeSet, TOKENIZER_VERSION);
|
|
2370
|
+
const lexicalHits = search(index, text, {
|
|
2371
|
+
limit: activeSet.length,
|
|
2372
|
+
minScorePermille: 0,
|
|
2373
|
+
...options.kinds === void 0 ? {} : { kinds: options.kinds }
|
|
2374
|
+
});
|
|
2375
|
+
const lexical = new Map(lexicalHits.map((hit) => [hit.id, hit.score]));
|
|
2376
|
+
const kindFilter = options.kinds === void 0 ? void 0 : new Set(options.kinds);
|
|
2377
|
+
const vector = /* @__PURE__ */ new Map();
|
|
2378
|
+
for (const doc of activeSet) {
|
|
2379
|
+
if (kindFilter !== void 0 && !kindFilter.has(doc.kind)) continue;
|
|
2380
|
+
const cached = cache.docs[doc.id];
|
|
2381
|
+
if (cached === void 0 || cached.length !== queryVector.length) continue;
|
|
2382
|
+
let dot = 0;
|
|
2383
|
+
for (let at = 0; at < queryVector.length; at += 1) dot += (queryVector[at] ?? 0) * (cached[at] ?? 0);
|
|
2384
|
+
vector.set(doc.id, Math.max(0, Math.min(1, dot)));
|
|
2385
|
+
}
|
|
2386
|
+
const minScore = this.config.recall.min_score / 1e3;
|
|
2387
|
+
const byId = new Map(activeSet.map((doc) => [doc.id, doc]));
|
|
2388
|
+
const ranked = [...fuseScores(lexical, vector, this.config.recall.embedding_weight).entries()].filter(([, score]) => score >= minScore).map(([id, score]) => ({ id, score })).sort((left, right) => {
|
|
2389
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
2390
|
+
const leftDoc = byId.get(left.id);
|
|
2391
|
+
const rightDoc = byId.get(right.id);
|
|
2392
|
+
if (leftDoc !== void 0 && rightDoc !== void 0 && rightDoc.valid_from !== leftDoc.valid_from) {
|
|
2393
|
+
return rightDoc.valid_from.localeCompare(leftDoc.valid_from);
|
|
2394
|
+
}
|
|
2395
|
+
return left.id.localeCompare(right.id);
|
|
2396
|
+
}).slice(0, limit);
|
|
2397
|
+
const hits = [];
|
|
2398
|
+
for (const hit of ranked) {
|
|
2399
|
+
const doc = byId.get(hit.id);
|
|
2400
|
+
if (doc === void 0) continue;
|
|
2401
|
+
hits.push({
|
|
2402
|
+
id: doc.id,
|
|
2403
|
+
kind: doc.kind,
|
|
2404
|
+
abstract: doc.abstract,
|
|
2405
|
+
score: hit.score,
|
|
2406
|
+
valid_from: doc.valid_from,
|
|
2407
|
+
valid_until: doc.valid_until,
|
|
2408
|
+
provenance_verifiable: doc.provenance.length > 0
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
return { hits, warnings: this.quarantined.size === 0 ? [] : [...this.quarantined.values()].slice(0, 5) };
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
const fallback = this.recall(query, options);
|
|
2414
|
+
return {
|
|
2415
|
+
...fallback,
|
|
2416
|
+
warnings: [
|
|
2417
|
+
...fallback.warnings,
|
|
2418
|
+
`hybrid vectorizer failed (${error instanceof Error ? error.message : String(error)}); fell back to lexical`
|
|
2419
|
+
]
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Move records between project labels by rewriting only the `project` field.
|
|
2425
|
+
* This is an organisational change, not a supersede: the id and history stay,
|
|
2426
|
+
* and the journal records the new content hash plus an audit line.
|
|
2427
|
+
*/
|
|
2428
|
+
moveProjects(ids, project) {
|
|
2429
|
+
const projectIssue = projectProblem(project);
|
|
2430
|
+
if (projectIssue !== void 0) throw new MemoryStoreError("INVALID", projectIssue);
|
|
2431
|
+
const lock = acquireLock({
|
|
2432
|
+
file: this.paths.lockFile,
|
|
2433
|
+
config: {
|
|
2434
|
+
heartbeatMs: this.config.lock.heartbeat_ms,
|
|
2435
|
+
staleMs: this.config.lock.stale_ms,
|
|
2436
|
+
timeoutMs: this.config.lock.timeout_ms
|
|
2437
|
+
},
|
|
2438
|
+
now: () => this.now().getTime()
|
|
2439
|
+
});
|
|
2440
|
+
try {
|
|
2441
|
+
this.reload();
|
|
2442
|
+
if (this.issues.length > 0) {
|
|
2443
|
+
throw new MemoryStoreError("CONFLICT", `journal is inconsistent, writes are blocked: ${this.issues[0]}`);
|
|
2444
|
+
}
|
|
2445
|
+
const moved = [];
|
|
2446
|
+
const failed = [];
|
|
2447
|
+
for (const id of ids) {
|
|
2448
|
+
const doc = this.docs.get(id);
|
|
2449
|
+
if (doc === void 0) {
|
|
2450
|
+
failed.push({ id, reason: "not found" });
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
const next = {
|
|
2454
|
+
id: doc.id,
|
|
2455
|
+
mem_schema: doc.mem_schema,
|
|
2456
|
+
kind: doc.kind,
|
|
2457
|
+
scope: doc.scope,
|
|
2458
|
+
project,
|
|
2459
|
+
tags: doc.tags ?? [],
|
|
2460
|
+
relations: doc.relations ?? [],
|
|
2461
|
+
source: doc.source ?? null,
|
|
2462
|
+
abstract: doc.abstract,
|
|
2463
|
+
created_at: doc.created_at,
|
|
2464
|
+
valid_from: doc.valid_from,
|
|
2465
|
+
valid_until: doc.valid_until,
|
|
2466
|
+
supersedes: doc.supersedes,
|
|
2467
|
+
author: doc.author,
|
|
2468
|
+
evidence_level: doc.evidence_level,
|
|
2469
|
+
error_signatures: doc.error_signatures,
|
|
2470
|
+
provenance: doc.provenance,
|
|
2471
|
+
body: doc.body
|
|
2472
|
+
};
|
|
2473
|
+
const text = serializeMemory(next);
|
|
2474
|
+
const file = join5(this.paths.memoriesDir, doc.kind, `${doc.id}.md`);
|
|
2475
|
+
this.beforeStep?.("before-write");
|
|
2476
|
+
writeFileAtomic(file, text);
|
|
2477
|
+
const hash = hashContent(text);
|
|
2478
|
+
this.journal.append({ op: "record", data: { id, sha256: hash } });
|
|
2479
|
+
this.journal.append({ op: "audit", data: { event: "project-move", id, project } });
|
|
2480
|
+
moved.push(id);
|
|
2481
|
+
}
|
|
2482
|
+
this.reload();
|
|
2483
|
+
this.writeDerived();
|
|
2484
|
+
return { moved, failed };
|
|
2485
|
+
} finally {
|
|
2486
|
+
lock.release();
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
/** Update tags/relations in place (organisational metadata, not a revision). */
|
|
2490
|
+
updateMeta(id, patch) {
|
|
2491
|
+
if (patch.tags !== void 0) {
|
|
2492
|
+
const issue = tagsProblem(patch.tags);
|
|
2493
|
+
if (issue !== void 0) throw new MemoryStoreError("INVALID", issue);
|
|
2494
|
+
}
|
|
2495
|
+
if (patch.relations !== void 0) {
|
|
2496
|
+
const issue = relationsProblem(patch.relations, id);
|
|
2497
|
+
if (issue !== void 0) throw new MemoryStoreError("INVALID", issue);
|
|
2498
|
+
}
|
|
2499
|
+
const lock = acquireLock({
|
|
2500
|
+
file: this.paths.lockFile,
|
|
2501
|
+
config: {
|
|
2502
|
+
heartbeatMs: this.config.lock.heartbeat_ms,
|
|
2503
|
+
staleMs: this.config.lock.stale_ms,
|
|
2504
|
+
timeoutMs: this.config.lock.timeout_ms
|
|
2505
|
+
},
|
|
2506
|
+
now: () => this.now().getTime()
|
|
2507
|
+
});
|
|
2508
|
+
try {
|
|
2509
|
+
this.reload();
|
|
2510
|
+
if (this.issues.length > 0) {
|
|
2511
|
+
throw new MemoryStoreError("CONFLICT", `journal is inconsistent, writes are blocked: ${this.issues[0]}`);
|
|
2512
|
+
}
|
|
2513
|
+
const doc = this.docs.get(id);
|
|
2514
|
+
if (doc === void 0) throw new MemoryStoreError("NOT_FOUND", `no memory with id ${id}`);
|
|
2515
|
+
const next = {
|
|
2516
|
+
id: doc.id,
|
|
2517
|
+
mem_schema: doc.mem_schema,
|
|
2518
|
+
kind: doc.kind,
|
|
2519
|
+
scope: doc.scope,
|
|
2520
|
+
project: doc.project ?? null,
|
|
2521
|
+
tags: patch.tags ?? doc.tags ?? [],
|
|
2522
|
+
relations: patch.relations ?? doc.relations ?? [],
|
|
2523
|
+
source: doc.source ?? null,
|
|
2524
|
+
abstract: doc.abstract,
|
|
2525
|
+
created_at: doc.created_at,
|
|
2526
|
+
valid_from: doc.valid_from,
|
|
2527
|
+
valid_until: doc.valid_until,
|
|
2528
|
+
supersedes: doc.supersedes,
|
|
2529
|
+
author: doc.author,
|
|
2530
|
+
evidence_level: doc.evidence_level,
|
|
2531
|
+
error_signatures: doc.error_signatures,
|
|
2532
|
+
provenance: doc.provenance,
|
|
2533
|
+
body: doc.body
|
|
2534
|
+
};
|
|
2535
|
+
const text = serializeMemory(next);
|
|
2536
|
+
writeFileAtomic(join5(this.paths.memoriesDir, doc.kind, `${doc.id}.md`), text);
|
|
2537
|
+
this.journal.append({ op: "record", data: { id, sha256: hashContent(text) } });
|
|
2538
|
+
this.journal.append({ op: "audit", data: { event: "memory-meta", id } });
|
|
2539
|
+
this.reload();
|
|
2540
|
+
this.writeDerived();
|
|
2541
|
+
return this.docs.get(id);
|
|
2542
|
+
} finally {
|
|
2543
|
+
lock.release();
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
1999
2546
|
stats() {
|
|
2000
2547
|
return {
|
|
2001
2548
|
active: this.active().length,
|
|
@@ -2047,11 +2594,17 @@ var MemoryStore = class {
|
|
|
2047
2594
|
throw new MemoryStoreError("INVALID", `valid_from must be a real YYYY-MM-DD date, found ${validFrom}`);
|
|
2048
2595
|
}
|
|
2049
2596
|
const duplicateId = this.allocateId(now);
|
|
2597
|
+
const relationIssue = relationsProblem(input.relations ?? [], duplicateId);
|
|
2598
|
+
if (relationIssue !== void 0) throw new MemoryStoreError("INVALID", relationIssue);
|
|
2050
2599
|
const doc = {
|
|
2051
2600
|
id: duplicateId,
|
|
2052
|
-
mem_schema:
|
|
2601
|
+
mem_schema: 2,
|
|
2053
2602
|
kind: input.kind,
|
|
2054
2603
|
scope: "local",
|
|
2604
|
+
project: input.project ?? null,
|
|
2605
|
+
tags: input.tags ?? [],
|
|
2606
|
+
relations: input.relations ?? [],
|
|
2607
|
+
source: input.source ?? null,
|
|
2055
2608
|
abstract: input.abstract,
|
|
2056
2609
|
created_at: now.toISOString(),
|
|
2057
2610
|
valid_from: validFrom,
|
|
@@ -2064,9 +2617,9 @@ var MemoryStore = class {
|
|
|
2064
2617
|
body: input.body
|
|
2065
2618
|
};
|
|
2066
2619
|
const text = serializeMemory(doc);
|
|
2067
|
-
const filePath =
|
|
2620
|
+
const filePath = join5(this.paths.memoriesDir, doc.kind, `${doc.id}.md`);
|
|
2068
2621
|
this.beforeStep?.("before-write");
|
|
2069
|
-
ensureDir(
|
|
2622
|
+
ensureDir(join5(this.paths.memoriesDir, doc.kind));
|
|
2070
2623
|
writeFileAtomic(filePath, text);
|
|
2071
2624
|
this.beforeStep?.("before-journal");
|
|
2072
2625
|
const hash = hashContent(text);
|
|
@@ -2114,7 +2667,7 @@ var MemoryStore = class {
|
|
|
2114
2667
|
if (file === void 0) {
|
|
2115
2668
|
throw new MemoryStoreError("NOT_FOUND", `no memory file for ${id}`);
|
|
2116
2669
|
}
|
|
2117
|
-
const text =
|
|
2670
|
+
const text = readFileSync7(file, "utf8");
|
|
2118
2671
|
let doc;
|
|
2119
2672
|
try {
|
|
2120
2673
|
doc = parseMemoryFile(text, file, {
|
|
@@ -2237,11 +2790,17 @@ var MemoryStore = class {
|
|
|
2237
2790
|
this.docs
|
|
2238
2791
|
);
|
|
2239
2792
|
if (problems.length > 0) throw new MemoryStoreError("INVALID", problems.join("; "));
|
|
2793
|
+
const relationIssue = relationsProblem(input.relations ?? [], id);
|
|
2794
|
+
if (relationIssue !== void 0) throw new MemoryStoreError("INVALID", relationIssue);
|
|
2240
2795
|
const doc = {
|
|
2241
2796
|
id,
|
|
2242
|
-
mem_schema:
|
|
2797
|
+
mem_schema: 2,
|
|
2243
2798
|
kind: input.kind,
|
|
2244
2799
|
scope: "local",
|
|
2800
|
+
project: input.project ?? null,
|
|
2801
|
+
tags: input.tags ?? [],
|
|
2802
|
+
relations: input.relations ?? [],
|
|
2803
|
+
source: input.source ?? null,
|
|
2245
2804
|
abstract: input.abstract,
|
|
2246
2805
|
created_at: now.toISOString(),
|
|
2247
2806
|
valid_from: input.valid_from ?? today(now),
|
|
@@ -2259,13 +2818,13 @@ var MemoryStore = class {
|
|
|
2259
2818
|
const written = [];
|
|
2260
2819
|
try {
|
|
2261
2820
|
for (const entry of prepared) {
|
|
2262
|
-
const file =
|
|
2263
|
-
ensureDir(
|
|
2821
|
+
const file = join5(this.paths.memoriesDir, entry.kind, `${entry.id}.md`);
|
|
2822
|
+
ensureDir(join5(this.paths.memoriesDir, entry.kind));
|
|
2264
2823
|
writeFileAtomic(file, entry.text);
|
|
2265
2824
|
written.push(file);
|
|
2266
2825
|
}
|
|
2267
2826
|
} catch (error) {
|
|
2268
|
-
for (const file of written)
|
|
2827
|
+
for (const file of written) rmSync3(file, { force: true });
|
|
2269
2828
|
throw error;
|
|
2270
2829
|
}
|
|
2271
2830
|
this.beforeStep?.("before-journal");
|
|
@@ -2341,7 +2900,7 @@ var MemoryStore = class {
|
|
|
2341
2900
|
op: "delete",
|
|
2342
2901
|
data: { id, purged: true, reason: options.reason, sha256: doc?.content_hash ?? "" }
|
|
2343
2902
|
});
|
|
2344
|
-
|
|
2903
|
+
rmSync3(file, { force: true });
|
|
2345
2904
|
let proposals = 0;
|
|
2346
2905
|
let names = [];
|
|
2347
2906
|
try {
|
|
@@ -2350,11 +2909,11 @@ var MemoryStore = class {
|
|
|
2350
2909
|
names = [];
|
|
2351
2910
|
}
|
|
2352
2911
|
for (const name2 of names) {
|
|
2353
|
-
const path =
|
|
2912
|
+
const path = join5(this.paths.pendingDir, name2);
|
|
2354
2913
|
try {
|
|
2355
|
-
const proposal = JSON.parse(
|
|
2914
|
+
const proposal = JSON.parse(readFileSync7(path, "utf8"));
|
|
2356
2915
|
if (Array.isArray(proposal.targets) && proposal.targets.includes(id)) {
|
|
2357
|
-
|
|
2916
|
+
rmSync3(path, { force: true });
|
|
2358
2917
|
this.journal.append({
|
|
2359
2918
|
op: "audit",
|
|
2360
2919
|
data: { event: "proposal-invalidated", proposal: proposal.id ?? name2, target: id }
|
|
@@ -2445,6 +3004,12 @@ var MemoryStore = class {
|
|
|
2445
3004
|
if (!KINDS2.includes(input.kind)) {
|
|
2446
3005
|
throw new MemoryStoreError("INVALID", `kind must be one of ${KINDS2.join(", ")}`);
|
|
2447
3006
|
}
|
|
3007
|
+
const projectIssue = projectProblem(input.project ?? null);
|
|
3008
|
+
if (projectIssue !== void 0) throw new MemoryStoreError("INVALID", projectIssue);
|
|
3009
|
+
const tagsIssue = tagsProblem(input.tags ?? []);
|
|
3010
|
+
if (tagsIssue !== void 0) throw new MemoryStoreError("INVALID", tagsIssue);
|
|
3011
|
+
const sourceIssue = sourceProblem(input.source ?? null);
|
|
3012
|
+
if (sourceIssue !== void 0) throw new MemoryStoreError("INVALID", sourceIssue);
|
|
2448
3013
|
if (typeof input.abstract !== "string" || input.abstract.trim() === "") {
|
|
2449
3014
|
throw new MemoryStoreError("INVALID", "abstract must not be empty");
|
|
2450
3015
|
}
|
|
@@ -2490,8 +3055,8 @@ var MemoryStore = class {
|
|
|
2490
3055
|
}
|
|
2491
3056
|
findMemoryFile(id) {
|
|
2492
3057
|
for (const kind of KINDS2) {
|
|
2493
|
-
const file =
|
|
2494
|
-
if (
|
|
3058
|
+
const file = join5(this.paths.memoriesDir, kind, `${id}.md`);
|
|
3059
|
+
if (existsSync4(file)) return file;
|
|
2495
3060
|
}
|
|
2496
3061
|
return void 0;
|
|
2497
3062
|
}
|
|
@@ -2510,7 +3075,7 @@ var MemoryStore = class {
|
|
|
2510
3075
|
allocateId(now) {
|
|
2511
3076
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
2512
3077
|
const id = newMemoryId(now);
|
|
2513
|
-
const exists = this.docs.has(id) || this.accounted.has(id) ||
|
|
3078
|
+
const exists = this.docs.has(id) || this.accounted.has(id) || existsSync4(join5(this.paths.memoriesDir, "fact", `${id}.md`)) || existsSync4(join5(this.paths.memoriesDir, "pitfall", `${id}.md`)) || existsSync4(join5(this.paths.memoriesDir, "exp", `${id}.md`));
|
|
2514
3079
|
if (!exists) return id;
|
|
2515
3080
|
}
|
|
2516
3081
|
throw new MemoryStoreError("CONFLICT", "could not allocate a unique memory id");
|
|
@@ -2525,8 +3090,8 @@ function countPending(dir) {
|
|
|
2525
3090
|
}
|
|
2526
3091
|
|
|
2527
3092
|
// packages/core/src/rules/rules.ts
|
|
2528
|
-
import { existsSync as
|
|
2529
|
-
import { join as
|
|
3093
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8 } from "node:fs";
|
|
3094
|
+
import { join as join6 } from "node:path";
|
|
2530
3095
|
var HEADER = /^(\d+)\.\s*\[(机器门禁|模型指引|废止)\]\s*(.*)$/;
|
|
2531
3096
|
var EXTRA = /^\s+(check|test):\s*(.+)$/;
|
|
2532
3097
|
function parseRules(text) {
|
|
@@ -2581,9 +3146,9 @@ function parseRules(text) {
|
|
|
2581
3146
|
function checkSymbolExists(check, rootDir) {
|
|
2582
3147
|
const [path, symbol] = check.split("#");
|
|
2583
3148
|
if (path === void 0 || symbol === void 0 || path === "" || symbol === "") return false;
|
|
2584
|
-
const file =
|
|
2585
|
-
if (!
|
|
2586
|
-
return
|
|
3149
|
+
const file = join6(rootDir, path);
|
|
3150
|
+
if (!existsSync5(file)) return false;
|
|
3151
|
+
return readFileSync8(file, "utf8").includes(symbol);
|
|
2587
3152
|
}
|
|
2588
3153
|
function validateRules(rules, options) {
|
|
2589
3154
|
const errors = [];
|
|
@@ -2593,12 +3158,12 @@ function validateRules(rules, options) {
|
|
|
2593
3158
|
errors.push(`rule ${rule.number}: check "${rule.check ?? ""}" does not name an existing symbol`);
|
|
2594
3159
|
}
|
|
2595
3160
|
if (rule.test === void 0) continue;
|
|
2596
|
-
const testFile =
|
|
2597
|
-
if (!
|
|
3161
|
+
const testFile = join6(options.rootDir, rule.test);
|
|
3162
|
+
if (!existsSync5(testFile)) {
|
|
2598
3163
|
errors.push(`rule ${rule.number}: test file "${rule.test}" does not exist`);
|
|
2599
3164
|
continue;
|
|
2600
3165
|
}
|
|
2601
|
-
if (!/\[neg\]/.test(
|
|
3166
|
+
if (!/\[neg\]/.test(readFileSync8(testFile, "utf8"))) {
|
|
2602
3167
|
errors.push(`rule ${rule.number}: test file "${rule.test}" has no [neg] case`);
|
|
2603
3168
|
}
|
|
2604
3169
|
}
|
|
@@ -2610,8 +3175,8 @@ function parseAndValidateRules(text, options) {
|
|
|
2610
3175
|
}
|
|
2611
3176
|
function loadRules(home) {
|
|
2612
3177
|
const file = memoryPaths(home).rulesFile;
|
|
2613
|
-
if (!
|
|
2614
|
-
return parseRules(
|
|
3178
|
+
if (!existsSync5(file)) return [];
|
|
3179
|
+
return parseRules(readFileSync8(file, "utf8")).rules;
|
|
2615
3180
|
}
|
|
2616
3181
|
function renderRules(rules, options = {}) {
|
|
2617
3182
|
const lines = [];
|
|
@@ -2624,7 +3189,7 @@ function renderRules(rules, options = {}) {
|
|
|
2624
3189
|
}
|
|
2625
3190
|
function readRulesFile(home) {
|
|
2626
3191
|
const file = memoryPaths(home).rulesFile;
|
|
2627
|
-
return
|
|
3192
|
+
return existsSync5(file) ? readFileSync8(file, "utf8") : "";
|
|
2628
3193
|
}
|
|
2629
3194
|
function saveRulesFile(home, text) {
|
|
2630
3195
|
const parsed = parseAndValidateRules(text, { rootDir: home });
|
|
@@ -2636,19 +3201,19 @@ function saveRulesFile(home, text) {
|
|
|
2636
3201
|
|
|
2637
3202
|
// packages/core/src/inject/snapshot.ts
|
|
2638
3203
|
import { createHash as createHash4 } from "node:crypto";
|
|
2639
|
-
import { existsSync as
|
|
2640
|
-
import { join as
|
|
3204
|
+
import { existsSync as existsSync6, readFileSync as readFileSync9, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync4 } from "node:fs";
|
|
3205
|
+
import { join as join7 } from "node:path";
|
|
2641
3206
|
var PROMPT_SCHEMA_VERSION = 4;
|
|
2642
3207
|
var SNAPSHOT_KEEP = 20;
|
|
2643
3208
|
function snapshotPath(home, sessionId) {
|
|
2644
3209
|
const name2 = createHash4("sha256").update(sessionId, "utf8").digest("hex").slice(0, 32);
|
|
2645
|
-
return
|
|
3210
|
+
return join7(memoryPaths(home).snapshotsDir, `${name2}.json`);
|
|
2646
3211
|
}
|
|
2647
3212
|
function readSnapshot(home, sessionId) {
|
|
2648
3213
|
const file = snapshotPath(home, sessionId);
|
|
2649
|
-
if (!
|
|
3214
|
+
if (!existsSync6(file)) return null;
|
|
2650
3215
|
try {
|
|
2651
|
-
const record = JSON.parse(
|
|
3216
|
+
const record = JSON.parse(readFileSync9(file, "utf8"));
|
|
2652
3217
|
if (record.session_id !== sessionId || typeof record.text !== "string" || typeof record.memory_revision !== "string" || typeof record.prompt_schema_version !== "number") {
|
|
2653
3218
|
return null;
|
|
2654
3219
|
}
|
|
@@ -2671,10 +3236,10 @@ function pruneSnapshots(home, keep = SNAPSHOT_KEEP) {
|
|
|
2671
3236
|
return;
|
|
2672
3237
|
}
|
|
2673
3238
|
if (names.length <= keep) return;
|
|
2674
|
-
const byAge = names.map((name2) => ({ name: name2, mtime: statSync4(
|
|
3239
|
+
const byAge = names.map((name2) => ({ name: name2, mtime: statSync4(join7(dir, name2)).mtimeMs })).sort((left, right) => right.mtime - left.mtime);
|
|
2675
3240
|
for (const entry of byAge.slice(keep)) {
|
|
2676
3241
|
try {
|
|
2677
|
-
|
|
3242
|
+
rmSync4(join7(dir, entry.name), { force: true });
|
|
2678
3243
|
} catch {
|
|
2679
3244
|
}
|
|
2680
3245
|
}
|
|
@@ -2684,7 +3249,7 @@ function pruneSnapshots(home, keep = SNAPSHOT_KEEP) {
|
|
|
2684
3249
|
import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
|
|
2685
3250
|
|
|
2686
3251
|
// packages/core/src/inject/sections.ts
|
|
2687
|
-
import { existsSync as
|
|
3252
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10 } from "node:fs";
|
|
2688
3253
|
|
|
2689
3254
|
// packages/core/src/personalization/guidance.ts
|
|
2690
3255
|
function renderGuidance(mode) {
|
|
@@ -2711,7 +3276,7 @@ function truncate(text, budget) {
|
|
|
2711
3276
|
return `${points.slice(0, budget - 1).join("")}\u2026`;
|
|
2712
3277
|
}
|
|
2713
3278
|
function readIfExists(file) {
|
|
2714
|
-
return
|
|
3279
|
+
return existsSync7(file) ? readFileSync10(file, "utf8").replace(COMMENT, "").trim() : "";
|
|
2715
3280
|
}
|
|
2716
3281
|
var SECTION_ORDERS = { SOUL: 100, GUIDANCE: 110, RULES: 150, USER: 200, MEMORY: 300 };
|
|
2717
3282
|
function builtinSectionSources(context) {
|
|
@@ -2932,8 +3497,8 @@ ${escaped}
|
|
|
2932
3497
|
}
|
|
2933
3498
|
|
|
2934
3499
|
// packages/core/src/proposals/proposals.ts
|
|
2935
|
-
import { existsSync as
|
|
2936
|
-
import { join as
|
|
3500
|
+
import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync11, rmSync as rmSync5 } from "node:fs";
|
|
3501
|
+
import { join as join8 } from "node:path";
|
|
2937
3502
|
var ProposalError = class extends Error {
|
|
2938
3503
|
code;
|
|
2939
3504
|
constructor(code, message) {
|
|
@@ -2946,7 +3511,7 @@ function pendingDir(store) {
|
|
|
2946
3511
|
return store.paths.pendingDir;
|
|
2947
3512
|
}
|
|
2948
3513
|
function proposalPath(store, id) {
|
|
2949
|
-
return
|
|
3514
|
+
return join8(pendingDir(store), `${id}.json`);
|
|
2950
3515
|
}
|
|
2951
3516
|
function listProposals(store) {
|
|
2952
3517
|
let names;
|
|
@@ -2958,7 +3523,7 @@ function listProposals(store) {
|
|
|
2958
3523
|
const proposals = [];
|
|
2959
3524
|
for (const name2 of names) {
|
|
2960
3525
|
try {
|
|
2961
|
-
proposals.push(JSON.parse(
|
|
3526
|
+
proposals.push(JSON.parse(readFileSync11(join8(pendingDir(store), name2), "utf8")));
|
|
2962
3527
|
} catch {
|
|
2963
3528
|
}
|
|
2964
3529
|
}
|
|
@@ -2966,8 +3531,8 @@ function listProposals(store) {
|
|
|
2966
3531
|
}
|
|
2967
3532
|
function readProposal(store, id) {
|
|
2968
3533
|
const file = proposalPath(store, id);
|
|
2969
|
-
if (!
|
|
2970
|
-
return JSON.parse(
|
|
3534
|
+
if (!existsSync8(file)) throw new ProposalError("NOT_FOUND", `no pending proposal ${id}`);
|
|
3535
|
+
return JSON.parse(readFileSync11(file, "utf8"));
|
|
2971
3536
|
}
|
|
2972
3537
|
function currentHash(store, id) {
|
|
2973
3538
|
const doc = store.read(id);
|
|
@@ -3033,7 +3598,7 @@ function decideProposal(deps, input) {
|
|
|
3033
3598
|
const proposal = readProposal(store, input.id);
|
|
3034
3599
|
if (input.decision === "reject") {
|
|
3035
3600
|
store.appendJournal("decide", { proposal: proposal.id, decision: "reject", reason: input.reason ?? "" });
|
|
3036
|
-
|
|
3601
|
+
rmSync5(proposalPath(store, proposal.id), { force: true });
|
|
3037
3602
|
return;
|
|
3038
3603
|
}
|
|
3039
3604
|
if (proposal.op === "delete") {
|
|
@@ -3044,13 +3609,13 @@ function decideProposal(deps, input) {
|
|
|
3044
3609
|
}
|
|
3045
3610
|
store.remove(target, { reason: proposal.reason, baseHash: hash });
|
|
3046
3611
|
store.journal.append({ op: "decide", data: { proposal: proposal.id, decision: "accept" } });
|
|
3047
|
-
|
|
3612
|
+
rmSync5(proposalPath(store, proposal.id), { force: true });
|
|
3048
3613
|
return;
|
|
3049
3614
|
}
|
|
3050
3615
|
if (proposal.op === "create") {
|
|
3051
3616
|
store.addMany(proposal.replacements, { author: "human", opId: `decide-${proposal.id}` });
|
|
3052
3617
|
store.appendJournal("decide", { proposal: proposal.id, decision: "accept" });
|
|
3053
|
-
|
|
3618
|
+
rmSync5(proposalPath(store, proposal.id), { force: true });
|
|
3054
3619
|
return;
|
|
3055
3620
|
}
|
|
3056
3621
|
proposal.targets.forEach((target, index) => {
|
|
@@ -3063,7 +3628,7 @@ function decideProposal(deps, input) {
|
|
|
3063
3628
|
{ author: "human", opId: `decide-${proposal.id}` }
|
|
3064
3629
|
);
|
|
3065
3630
|
store.appendJournal("decide", { proposal: proposal.id, decision: "accept" });
|
|
3066
|
-
|
|
3631
|
+
rmSync5(proposalPath(store, proposal.id), { force: true });
|
|
3067
3632
|
}
|
|
3068
3633
|
|
|
3069
3634
|
// packages/core/src/actor.ts
|
|
@@ -3121,13 +3686,13 @@ var SessionQuota = class {
|
|
|
3121
3686
|
|
|
3122
3687
|
// packages/core/src/distill/closeout.ts
|
|
3123
3688
|
import { createHash as createHash7 } from "node:crypto";
|
|
3124
|
-
import { existsSync as
|
|
3125
|
-
import { join as
|
|
3689
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12 } from "node:fs";
|
|
3690
|
+
import { join as join9 } from "node:path";
|
|
3126
3691
|
function loadRejected(home) {
|
|
3127
|
-
const file =
|
|
3128
|
-
if (!
|
|
3692
|
+
const file = join9(memoryPaths(home).stateDir, "distill-rejected.json");
|
|
3693
|
+
if (!existsSync9(file)) return /* @__PURE__ */ new Set();
|
|
3129
3694
|
try {
|
|
3130
|
-
const parsed = JSON.parse(
|
|
3695
|
+
const parsed = JSON.parse(readFileSync12(file, "utf8"));
|
|
3131
3696
|
return new Set(Array.isArray(parsed.hashes) ? parsed.hashes.filter((value) => typeof value === "string") : []);
|
|
3132
3697
|
} catch {
|
|
3133
3698
|
return /* @__PURE__ */ new Set();
|
|
@@ -3137,7 +3702,7 @@ function addRejected(home, hashes) {
|
|
|
3137
3702
|
const set = loadRejected(home);
|
|
3138
3703
|
for (const hash of hashes) set.add(hash);
|
|
3139
3704
|
writeFileAtomic(
|
|
3140
|
-
|
|
3705
|
+
join9(memoryPaths(home).stateDir, "distill-rejected.json"),
|
|
3141
3706
|
`${JSON.stringify({ hashes: [...set].sort() }, null, 2)}
|
|
3142
3707
|
`
|
|
3143
3708
|
);
|
|
@@ -3381,8 +3946,8 @@ var CloseoutRound = class {
|
|
|
3381
3946
|
|
|
3382
3947
|
// packages/core/src/ops/ops.ts
|
|
3383
3948
|
import { createHash as createHash8 } from "node:crypto";
|
|
3384
|
-
import { cpSync, existsSync as
|
|
3385
|
-
import { join as
|
|
3949
|
+
import { cpSync, existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync13, readdirSync as readdirSync5, statSync as statSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3950
|
+
import { join as join10, relative } from "node:path";
|
|
3386
3951
|
var INCLUDE = [
|
|
3387
3952
|
"config.toml",
|
|
3388
3953
|
"policy.json",
|
|
@@ -3399,9 +3964,9 @@ function exportStore(options) {
|
|
|
3399
3964
|
const paths = memoryPaths(options.home);
|
|
3400
3965
|
mkdirSync3(options.out, { recursive: true });
|
|
3401
3966
|
for (const entry of INCLUDE) {
|
|
3402
|
-
const source =
|
|
3403
|
-
if (!
|
|
3404
|
-
const target =
|
|
3967
|
+
const source = join10(paths.home, entry);
|
|
3968
|
+
if (!existsSync10(source)) continue;
|
|
3969
|
+
const target = join10(options.out, entry);
|
|
3405
3970
|
cpSync(source, target, { recursive: true });
|
|
3406
3971
|
}
|
|
3407
3972
|
const lines = [];
|
|
@@ -3409,13 +3974,13 @@ function exportStore(options) {
|
|
|
3409
3974
|
let bytes = 0;
|
|
3410
3975
|
const walk2 = (dir, base) => {
|
|
3411
3976
|
for (const name2 of readdirSync5(dir)) {
|
|
3412
|
-
const full =
|
|
3977
|
+
const full = join10(dir, name2);
|
|
3413
3978
|
if (statSync5(full).isDirectory()) {
|
|
3414
3979
|
walk2(full, base);
|
|
3415
3980
|
continue;
|
|
3416
3981
|
}
|
|
3417
3982
|
const rel = relative(base, full).replaceAll("\\", "/");
|
|
3418
|
-
const data =
|
|
3983
|
+
const data = readFileSync13(full);
|
|
3419
3984
|
lines.push(`${createHash8("sha256").update(data).digest("hex")} ${rel}`);
|
|
3420
3985
|
files += 1;
|
|
3421
3986
|
bytes += data.length;
|
|
@@ -3423,8 +3988,8 @@ function exportStore(options) {
|
|
|
3423
3988
|
};
|
|
3424
3989
|
walk2(options.out, options.out);
|
|
3425
3990
|
lines.sort();
|
|
3426
|
-
const sums =
|
|
3427
|
-
|
|
3991
|
+
const sums = join10(options.out, "SHA256SUMS");
|
|
3992
|
+
writeFileSync4(sums, `${lines.join("\n")}
|
|
3428
3993
|
`, "utf8");
|
|
3429
3994
|
return { out: options.out, files, bytes, sums };
|
|
3430
3995
|
}
|
|
@@ -3432,8 +3997,8 @@ function personalizationChars(home) {
|
|
|
3432
3997
|
const paths = memoryPaths(home);
|
|
3433
3998
|
const charsOf = (file) => {
|
|
3434
3999
|
try {
|
|
3435
|
-
if (!
|
|
3436
|
-
return [...
|
|
4000
|
+
if (!existsSync10(file)) return 0;
|
|
4001
|
+
return [...readFileSync13(file, "utf8").replace(/<!--[\s\S]*?-->/g, "")].length;
|
|
3437
4002
|
} catch {
|
|
3438
4003
|
return 0;
|
|
3439
4004
|
}
|
|
@@ -3474,7 +4039,7 @@ function inspectStore(options) {
|
|
|
3474
4039
|
}
|
|
3475
4040
|
|
|
3476
4041
|
// packages/core/src/profile/profile.ts
|
|
3477
|
-
import { existsSync as
|
|
4042
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "node:fs";
|
|
3478
4043
|
var PROFILE_SEPARATOR = "\xA7";
|
|
3479
4044
|
var PROFILE_MAX_CHARS = 1e4;
|
|
3480
4045
|
var ProfileError = class extends Error {
|
|
@@ -3534,7 +4099,7 @@ function validateProfileEntries(entries) {
|
|
|
3534
4099
|
}
|
|
3535
4100
|
function loadProfile(home) {
|
|
3536
4101
|
const file = memoryPaths(home).userFile;
|
|
3537
|
-
const text =
|
|
4102
|
+
const text = existsSync11(file) ? readFileSync14(file, "utf8") : "";
|
|
3538
4103
|
const entries = parseProfileEntries(text);
|
|
3539
4104
|
return { entries, chars: profileChars(entries) };
|
|
3540
4105
|
}
|
|
@@ -3563,7 +4128,7 @@ function findProfileEntry(entries, oldText) {
|
|
|
3563
4128
|
}
|
|
3564
4129
|
|
|
3565
4130
|
// packages/core/src/personalization/scaffold.ts
|
|
3566
|
-
import { existsSync as
|
|
4131
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
3567
4132
|
var SOUL_TEMPLATE = `<!--
|
|
3568
4133
|
\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
4134
|
\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 +4160,7 @@ function scaffoldPersonalization(options) {
|
|
|
3595
4160
|
const created = [];
|
|
3596
4161
|
const kept = [];
|
|
3597
4162
|
for (const [file, template] of targets) {
|
|
3598
|
-
if (
|
|
4163
|
+
if (existsSync12(file)) {
|
|
3599
4164
|
kept.push(file);
|
|
3600
4165
|
continue;
|
|
3601
4166
|
}
|
|
@@ -3965,8 +4530,8 @@ var LEVELS = /* @__PURE__ */ new Set(["user_explicit", "user_confirmed", "observ
|
|
|
3965
4530
|
function importBundle(store, bundle) {
|
|
3966
4531
|
if (typeof bundle !== "object" || bundle === null) throw new Error("bundle must be an object");
|
|
3967
4532
|
const record = bundle;
|
|
3968
|
-
if (record.format !== "agent-memory-export-v1") {
|
|
3969
|
-
throw new Error("unsupported bundle format (expected agent-memory-export-v1)");
|
|
4533
|
+
if (record.format !== "agent-memory-export-v1" && record.format !== "agent-memory-export-v2") {
|
|
4534
|
+
throw new Error("unsupported bundle format (expected agent-memory-export-v1 or v2)");
|
|
3970
4535
|
}
|
|
3971
4536
|
if (!Array.isArray(record.memories)) throw new Error("bundle.memories must be an array");
|
|
3972
4537
|
let imported = 0;
|
|
@@ -3988,6 +4553,14 @@ function importBundle(store, bundle) {
|
|
|
3988
4553
|
kind: raw.kind,
|
|
3989
4554
|
abstract,
|
|
3990
4555
|
body,
|
|
4556
|
+
...typeof raw.project === "string" || raw.project === null ? { project: raw.project } : {},
|
|
4557
|
+
...Array.isArray(raw.tags) ? { tags: raw.tags.filter((entry) => typeof entry === "string") } : {},
|
|
4558
|
+
...Array.isArray(raw.relations) ? {
|
|
4559
|
+
relations: raw.relations.filter(
|
|
4560
|
+
(entry) => typeof entry === "object" && entry !== null && typeof entry.type === "string" && typeof entry.target === "string"
|
|
4561
|
+
)
|
|
4562
|
+
} : {},
|
|
4563
|
+
...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
4564
|
evidence_level: evidenceLevel,
|
|
3992
4565
|
...typeof raw.valid_from === "string" ? { valid_from: raw.valid_from } : {},
|
|
3993
4566
|
...Array.isArray(raw.error_signatures) ? { error_signatures: raw.error_signatures.filter((entry) => typeof entry === "string") } : {}
|
|
@@ -4037,6 +4610,8 @@ var TOOL_SCHEMAS = {
|
|
|
4037
4610
|
)
|
|
4038
4611
|
},
|
|
4039
4612
|
valid_from: { type: "string" },
|
|
4613
|
+
project: { type: "string" },
|
|
4614
|
+
tags: { type: "array", items: { type: "string" }, maxItems: 8 },
|
|
4040
4615
|
error_signatures: { type: "array", items: { type: "string" }, maxItems: 3 }
|
|
4041
4616
|
},
|
|
4042
4617
|
["kind", "abstract", "body", "evidence"]
|
|
@@ -4063,6 +4638,8 @@ var TOOL_SCHEMAS = {
|
|
|
4063
4638
|
)
|
|
4064
4639
|
},
|
|
4065
4640
|
valid_from: { type: "string" },
|
|
4641
|
+
project: { type: "string" },
|
|
4642
|
+
tags: { type: "array", items: { type: "string" }, maxItems: 8 },
|
|
4066
4643
|
error_signatures: { type: "array", items: { type: "string" }, maxItems: 3 }
|
|
4067
4644
|
},
|
|
4068
4645
|
["kind", "abstract", "body", "evidence"]
|
|
@@ -4804,6 +5381,10 @@ function summary(doc, serial) {
|
|
|
4804
5381
|
id: doc.id,
|
|
4805
5382
|
kind: doc.kind,
|
|
4806
5383
|
abstract: doc.abstract,
|
|
5384
|
+
project: doc.project ?? null,
|
|
5385
|
+
tags: doc.tags ?? [],
|
|
5386
|
+
relations: doc.relations ?? [],
|
|
5387
|
+
source: doc.source ?? null,
|
|
4807
5388
|
author: doc.author,
|
|
4808
5389
|
valid_from: doc.valid_from,
|
|
4809
5390
|
valid_until: doc.valid_until,
|
|
@@ -4837,8 +5418,8 @@ function buildItem(store, id) {
|
|
|
4837
5418
|
content_hash: doc.content_hash
|
|
4838
5419
|
};
|
|
4839
5420
|
}
|
|
4840
|
-
function buildSearch(store, query, limit = 20) {
|
|
4841
|
-
const result = store.
|
|
5421
|
+
async function buildSearch(store, query, limit = 20) {
|
|
5422
|
+
const result = await store.recallAsync(query, { limit });
|
|
4842
5423
|
const serials = serialsFor(store.active());
|
|
4843
5424
|
return {
|
|
4844
5425
|
query,
|
|
@@ -4853,6 +5434,73 @@ function buildSearch(store, query, limit = 20) {
|
|
|
4853
5434
|
warnings: result.warnings
|
|
4854
5435
|
};
|
|
4855
5436
|
}
|
|
5437
|
+
function buildGraph(store, home) {
|
|
5438
|
+
const active = store.active();
|
|
5439
|
+
const serials = serialsFor(active);
|
|
5440
|
+
const registered = listProjects(home).map((project) => project.name);
|
|
5441
|
+
const registeredSet = new Set(registered);
|
|
5442
|
+
const effectiveProject = (doc) => doc.project !== null && doc.project !== void 0 && registeredSet.has(doc.project) ? doc.project : "\u672A\u5F52\u7C7B";
|
|
5443
|
+
const names = new Set(registered);
|
|
5444
|
+
for (const doc of active) names.add(effectiveProject(doc));
|
|
5445
|
+
const projects = [...names].map((name2) => {
|
|
5446
|
+
const docs = active.filter((doc) => effectiveProject(doc) === name2);
|
|
5447
|
+
const last = docs.map((doc) => doc.valid_from).sort().at(-1) ?? null;
|
|
5448
|
+
return { name: name2, count: docs.length, last_updated: last };
|
|
5449
|
+
});
|
|
5450
|
+
projects.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
|
|
5451
|
+
const limited = active.slice().sort((left, right) => right.valid_from.localeCompare(left.valid_from) || right.created_at.localeCompare(left.created_at)).slice(0, 200);
|
|
5452
|
+
const ids = new Set(limited.map((doc) => doc.id));
|
|
5453
|
+
const nodes = limited.map((doc) => {
|
|
5454
|
+
const serial = serials.get(doc.id) ?? "";
|
|
5455
|
+
return {
|
|
5456
|
+
id: doc.id,
|
|
5457
|
+
serial,
|
|
5458
|
+
display: displayName(serial, doc.abstract),
|
|
5459
|
+
kind: doc.kind,
|
|
5460
|
+
project: effectiveProject(doc),
|
|
5461
|
+
tags: doc.tags ?? [],
|
|
5462
|
+
relations: (doc.relations ?? []).map((relation) => ({ type: relation.type, target: relation.target })),
|
|
5463
|
+
supersedes: doc.supersedes,
|
|
5464
|
+
valid_from: doc.valid_from
|
|
5465
|
+
};
|
|
5466
|
+
});
|
|
5467
|
+
const edges = [];
|
|
5468
|
+
for (const doc of limited) {
|
|
5469
|
+
for (const target of doc.supersedes) {
|
|
5470
|
+
if (ids.has(target)) edges.push({ from: doc.id, to: target, type: "supersedes" });
|
|
5471
|
+
}
|
|
5472
|
+
for (const relation of doc.relations ?? []) {
|
|
5473
|
+
if (ids.has(relation.target)) edges.push({ from: doc.id, to: relation.target, type: relation.type });
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
const conflicts = [];
|
|
5477
|
+
for (let left = 0; left < limited.length; left += 1) {
|
|
5478
|
+
for (let right = left + 1; right < limited.length; right += 1) {
|
|
5479
|
+
const a = limited[left];
|
|
5480
|
+
const b = limited[right];
|
|
5481
|
+
if (effectiveProject(a) !== effectiveProject(b)) continue;
|
|
5482
|
+
const shared = (a.tags ?? []).some((tag) => (b.tags ?? []).includes(tag));
|
|
5483
|
+
if (!shared) continue;
|
|
5484
|
+
const charsA = /* @__PURE__ */ new Set([...a.abstract.replace(/\s+/g, "")]);
|
|
5485
|
+
const charsB = /* @__PURE__ */ new Set([...b.abstract.replace(/\s+/g, "")]);
|
|
5486
|
+
let overlap = 0;
|
|
5487
|
+
for (const char of charsA) if (charsB.has(char)) overlap += 1;
|
|
5488
|
+
const similarity = overlap / Math.max(1, (/* @__PURE__ */ new Set([...charsA, ...charsB])).size);
|
|
5489
|
+
if (similarity >= 0.8) {
|
|
5490
|
+
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" });
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
return {
|
|
5495
|
+
schema: 1,
|
|
5496
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5497
|
+
truncated: active.length > limited.length,
|
|
5498
|
+
projects,
|
|
5499
|
+
nodes,
|
|
5500
|
+
edges,
|
|
5501
|
+
conflicts
|
|
5502
|
+
};
|
|
5503
|
+
}
|
|
4856
5504
|
function exportBundle(store) {
|
|
4857
5505
|
const active = store.active();
|
|
4858
5506
|
const serials = serialsFor(active);
|
|
@@ -4864,7 +5512,7 @@ function exportBundle(store) {
|
|
|
4864
5512
|
content_hash: doc.content_hash
|
|
4865
5513
|
}));
|
|
4866
5514
|
return {
|
|
4867
|
-
format: "agent-memory-export-
|
|
5515
|
+
format: "agent-memory-export-v2",
|
|
4868
5516
|
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4869
5517
|
memories,
|
|
4870
5518
|
proposals: listProposals(store).map((proposal) => ({
|
|
@@ -4974,7 +5622,11 @@ function saveRulesGui(home, store, text) {
|
|
|
4974
5622
|
function json(value, status = 200) {
|
|
4975
5623
|
return new Response(JSON.stringify(value), {
|
|
4976
5624
|
status,
|
|
4977
|
-
headers: {
|
|
5625
|
+
headers: {
|
|
5626
|
+
"content-type": "application/json; charset=utf-8",
|
|
5627
|
+
"cache-control": "no-store",
|
|
5628
|
+
"x-agent-memory-api-version": "1"
|
|
5629
|
+
}
|
|
4978
5630
|
});
|
|
4979
5631
|
}
|
|
4980
5632
|
function errorResponse(message, status = 400) {
|
|
@@ -5012,6 +5664,87 @@ function guiRoutes(deps) {
|
|
|
5012
5664
|
requestBody: "buffered",
|
|
5013
5665
|
fetch: async () => json(buildList(deps.store))
|
|
5014
5666
|
};
|
|
5667
|
+
const graph = {
|
|
5668
|
+
path: "/api/agent-memory/graph",
|
|
5669
|
+
methods: ["GET"],
|
|
5670
|
+
requestBody: "buffered",
|
|
5671
|
+
fetch: async () => json(buildGraph(deps.store, deps.home))
|
|
5672
|
+
};
|
|
5673
|
+
const projects = {
|
|
5674
|
+
path: "/api/agent-memory/projects",
|
|
5675
|
+
methods: ["GET", "POST", "DELETE"],
|
|
5676
|
+
requestBody: "buffered",
|
|
5677
|
+
fetch: async (request) => {
|
|
5678
|
+
if (request.method === "GET") return json({ projects: listProjects(deps.home) });
|
|
5679
|
+
if (request.method === "DELETE") {
|
|
5680
|
+
const name2 = new URL(request.url).searchParams.get("name") ?? "";
|
|
5681
|
+
if (name2 === "") return errorResponse("name is required");
|
|
5682
|
+
return deleteProject(deps.home, name2) ? json({ ok: true }) : errorResponse("project not found", 404);
|
|
5683
|
+
}
|
|
5684
|
+
const body = await readJson(request);
|
|
5685
|
+
if (typeof body.name !== "string" || body.name.trim() === "") return errorResponse("name is required");
|
|
5686
|
+
try {
|
|
5687
|
+
const project = createProject({
|
|
5688
|
+
home: deps.home,
|
|
5689
|
+
name: body.name,
|
|
5690
|
+
...typeof body.description === "string" ? { description: body.description } : {}
|
|
5691
|
+
});
|
|
5692
|
+
return json({ project });
|
|
5693
|
+
} catch (error) {
|
|
5694
|
+
return errorResponse(error instanceof Error ? error.message : String(error), 409);
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
};
|
|
5698
|
+
const moveProject = {
|
|
5699
|
+
path: "/api/agent-memory/projects/move",
|
|
5700
|
+
methods: ["POST"],
|
|
5701
|
+
requestBody: "buffered",
|
|
5702
|
+
fetch: async (request) => {
|
|
5703
|
+
const body = await readJson(request);
|
|
5704
|
+
if (!Array.isArray(body.ids) || body.ids.some((id) => typeof id !== "string")) {
|
|
5705
|
+
return errorResponse("ids must be a string array");
|
|
5706
|
+
}
|
|
5707
|
+
if (body.project !== null && typeof body.project !== "string") {
|
|
5708
|
+
return errorResponse("project must be a string or null");
|
|
5709
|
+
}
|
|
5710
|
+
const target = body.project;
|
|
5711
|
+
if (target !== null && !listProjects(deps.home).some((project) => project.name === target)) {
|
|
5712
|
+
return errorResponse(`unknown project: ${target}`, 409);
|
|
5713
|
+
}
|
|
5714
|
+
try {
|
|
5715
|
+
const result = deps.store.moveProjects(body.ids, target);
|
|
5716
|
+
return json({ ok: true, ...result });
|
|
5717
|
+
} catch (error) {
|
|
5718
|
+
return errorResponse(error instanceof Error ? error.message : String(error), 409);
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
};
|
|
5722
|
+
const meta = {
|
|
5723
|
+
path: "/api/agent-memory/meta",
|
|
5724
|
+
methods: ["POST"],
|
|
5725
|
+
requestBody: "buffered",
|
|
5726
|
+
fetch: async (request) => {
|
|
5727
|
+
const body = await readJson(request);
|
|
5728
|
+
if (typeof body.id !== "string" || body.id === "") return errorResponse("id is required");
|
|
5729
|
+
if (body.tags !== void 0 && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== "string"))) {
|
|
5730
|
+
return errorResponse("tags must be a string array");
|
|
5731
|
+
}
|
|
5732
|
+
if (body.relations !== void 0 && (!Array.isArray(body.relations) || body.relations.some(
|
|
5733
|
+
(relation) => typeof relation !== "object" || relation === null || typeof relation.type !== "string" || typeof relation.target !== "string"
|
|
5734
|
+
))) {
|
|
5735
|
+
return errorResponse("relations must be {type,target} objects");
|
|
5736
|
+
}
|
|
5737
|
+
try {
|
|
5738
|
+
const doc = deps.store.updateMeta(body.id, {
|
|
5739
|
+
...body.tags === void 0 ? {} : { tags: body.tags },
|
|
5740
|
+
...body.relations === void 0 ? {} : { relations: body.relations }
|
|
5741
|
+
});
|
|
5742
|
+
return json({ ok: true, id: doc.id, project: doc.project ?? null, tags: doc.tags ?? [], relations: doc.relations ?? [] });
|
|
5743
|
+
} catch (error) {
|
|
5744
|
+
return errorResponse(error instanceof Error ? error.message : String(error), 409);
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
};
|
|
5015
5748
|
const item = {
|
|
5016
5749
|
path: "/api/agent-memory/item",
|
|
5017
5750
|
methods: ["GET"],
|
|
@@ -5033,7 +5766,7 @@ function guiRoutes(deps) {
|
|
|
5033
5766
|
fetch: async (request) => {
|
|
5034
5767
|
const query = (new URL(request.url).searchParams.get("q") ?? "").trim();
|
|
5035
5768
|
if (query === "") return errorResponse("q is required");
|
|
5036
|
-
return json(buildSearch(deps.store, query));
|
|
5769
|
+
return json(await buildSearch(deps.store, query));
|
|
5037
5770
|
}
|
|
5038
5771
|
};
|
|
5039
5772
|
const exportRoute = {
|
|
@@ -5209,6 +5942,10 @@ function guiRoutes(deps) {
|
|
|
5209
5942
|
return [
|
|
5210
5943
|
status,
|
|
5211
5944
|
list,
|
|
5945
|
+
graph,
|
|
5946
|
+
projects,
|
|
5947
|
+
moveProject,
|
|
5948
|
+
meta,
|
|
5212
5949
|
item,
|
|
5213
5950
|
search2,
|
|
5214
5951
|
exportRoute,
|
|
@@ -5292,8 +6029,8 @@ function openHostService(home, session, options = {}) {
|
|
|
5292
6029
|
mode: runtime.config.personalization.mode,
|
|
5293
6030
|
session,
|
|
5294
6031
|
config: runtime.config,
|
|
5295
|
-
recall(input) {
|
|
5296
|
-
return store.
|
|
6032
|
+
async recall(input) {
|
|
6033
|
+
return store.recallAsync(input.query, {
|
|
5297
6034
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
5298
6035
|
...input.kind === void 0 ? {} : { kinds: [input.kind] }
|
|
5299
6036
|
});
|
|
@@ -5324,6 +6061,8 @@ function openHostService(home, session, options = {}) {
|
|
|
5324
6061
|
body: raw.body,
|
|
5325
6062
|
evidence_level: "observed",
|
|
5326
6063
|
...raw.valid_from === void 0 ? {} : { valid_from: raw.valid_from },
|
|
6064
|
+
...raw.project === void 0 ? {} : { project: raw.project },
|
|
6065
|
+
...raw.tags === void 0 ? {} : { tags: raw.tags },
|
|
5327
6066
|
...raw.error_signatures === void 0 ? {} : { error_signatures: raw.error_signatures }
|
|
5328
6067
|
},
|
|
5329
6068
|
{ author: "agent", provenance }
|
|
@@ -5372,6 +6111,8 @@ function openHostService(home, session, options = {}) {
|
|
|
5372
6111
|
body: replacement.body,
|
|
5373
6112
|
evidence_level: replacement.evidence_level ?? "observed",
|
|
5374
6113
|
...replacement.valid_from === void 0 ? {} : { valid_from: replacement.valid_from },
|
|
6114
|
+
...replacement.project === void 0 ? {} : { project: replacement.project },
|
|
6115
|
+
...replacement.tags === void 0 ? {} : { tags: replacement.tags },
|
|
5375
6116
|
...replacement.error_signatures === void 0 ? {} : { error_signatures: replacement.error_signatures },
|
|
5376
6117
|
provenance
|
|
5377
6118
|
});
|