@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/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) fail("mem_schema", "must be 1", filePath);
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: 1,
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 readFileSync3, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync2, writeSync } from "node:fs";
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(readFileSync3(file, "utf8"));
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 readFileSync4, readdirSync, statSync as statSync3, writeSync as writeSync2 } from "node:fs";
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 = readFileSync4(file, "utf8").split("\n").filter((line) => line !== "");
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 = readFileSync4(file, "utf8");
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 existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync2 } from "node:fs";
1609
- import { join as join4 } from "node:path";
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 = join4(this.paths.memoriesDir, kind);
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 = join4(dir, name2);
2195
+ const file = join5(dir, name2);
1886
2196
  const id = name2.slice(0, -3);
1887
2197
  try {
1888
- const text = readFileSync5(file, "utf8");
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(readFileSync5(this.paths.manifestFile, "utf8"));
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 found) {
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: 1,
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 = join4(this.paths.memoriesDir, doc.kind, `${doc.id}.md`);
2620
+ const filePath = join5(this.paths.memoriesDir, doc.kind, `${doc.id}.md`);
2068
2621
  this.beforeStep?.("before-write");
2069
- ensureDir(join4(this.paths.memoriesDir, doc.kind));
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 = readFileSync5(file, "utf8");
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: 1,
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 = join4(this.paths.memoriesDir, entry.kind, `${entry.id}.md`);
2263
- ensureDir(join4(this.paths.memoriesDir, entry.kind));
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) rmSync2(file, { force: true });
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
- rmSync2(file, { force: true });
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 = join4(this.paths.pendingDir, name2);
2912
+ const path = join5(this.paths.pendingDir, name2);
2354
2913
  try {
2355
- const proposal = JSON.parse(readFileSync5(path, "utf8"));
2914
+ const proposal = JSON.parse(readFileSync7(path, "utf8"));
2356
2915
  if (Array.isArray(proposal.targets) && proposal.targets.includes(id)) {
2357
- rmSync2(path, { force: true });
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 = join4(this.paths.memoriesDir, kind, `${id}.md`);
2494
- if (existsSync3(file)) return file;
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) || existsSync3(join4(this.paths.memoriesDir, "fact", `${id}.md`)) || existsSync3(join4(this.paths.memoriesDir, "pitfall", `${id}.md`)) || existsSync3(join4(this.paths.memoriesDir, "exp", `${id}.md`));
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 existsSync4, readFileSync as readFileSync6 } from "node:fs";
2529
- import { join as join5 } from "node:path";
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 = join5(rootDir, path);
2585
- if (!existsSync4(file)) return false;
2586
- return readFileSync6(file, "utf8").includes(symbol);
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 = join5(options.rootDir, rule.test);
2597
- if (!existsSync4(testFile)) {
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(readFileSync6(testFile, "utf8"))) {
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 (!existsSync4(file)) return [];
2614
- return parseRules(readFileSync6(file, "utf8")).rules;
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 existsSync4(file) ? readFileSync6(file, "utf8") : "";
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 existsSync5, readFileSync as readFileSync7, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
2640
- import { join as join6 } from "node:path";
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 join6(memoryPaths(home).snapshotsDir, `${name2}.json`);
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 (!existsSync5(file)) return null;
3214
+ if (!existsSync6(file)) return null;
2650
3215
  try {
2651
- const record = JSON.parse(readFileSync7(file, "utf8"));
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(join6(dir, name2)).mtimeMs })).sort((left, right) => right.mtime - left.mtime);
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
- rmSync3(join6(dir, entry.name), { force: true });
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 existsSync6, readFileSync as readFileSync8 } from "node:fs";
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 existsSync6(file) ? readFileSync8(file, "utf8").replace(COMMENT, "").trim() : "";
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 existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync9, rmSync as rmSync4 } from "node:fs";
2936
- import { join as join7 } from "node:path";
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 join7(pendingDir(store), `${id}.json`);
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(readFileSync9(join7(pendingDir(store), name2), "utf8")));
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 (!existsSync7(file)) throw new ProposalError("NOT_FOUND", `no pending proposal ${id}`);
2970
- return JSON.parse(readFileSync9(file, "utf8"));
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
- rmSync4(proposalPath(store, proposal.id), { force: true });
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
- rmSync4(proposalPath(store, proposal.id), { force: true });
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
- rmSync4(proposalPath(store, proposal.id), { force: true });
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
- rmSync4(proposalPath(store, proposal.id), { force: true });
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 existsSync8, readFileSync as readFileSync10 } from "node:fs";
3125
- import { join as join8 } from "node:path";
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 = join8(memoryPaths(home).stateDir, "distill-rejected.json");
3128
- if (!existsSync8(file)) return /* @__PURE__ */ new Set();
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(readFileSync10(file, "utf8"));
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
- join8(memoryPaths(home).stateDir, "distill-rejected.json"),
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 existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync11, readdirSync as readdirSync5, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
3385
- import { join as join9, relative } from "node:path";
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 = join9(paths.home, entry);
3403
- if (!existsSync9(source)) continue;
3404
- const target = join9(options.out, entry);
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 = join9(dir, name2);
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 = readFileSync11(full);
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 = join9(options.out, "SHA256SUMS");
3427
- writeFileSync3(sums, `${lines.join("\n")}
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 (!existsSync9(file)) return 0;
3436
- return [...readFileSync11(file, "utf8").replace(/<!--[\s\S]*?-->/g, "")].length;
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 existsSync10, readFileSync as readFileSync12 } from "node:fs";
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 = existsSync10(file) ? readFileSync12(file, "utf8") : "";
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 existsSync11 } from "node:fs";
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 (existsSync11(file)) {
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.recall(query, { limit });
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-v1",
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: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }
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.recall(input.query, {
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
  });