@agent_memory/dsh 0.2.1 → 0.5.0

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