@danielsimonjr/memoryjs 1.9.0 → 1.15.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/cli/index.js CHANGED
@@ -495,7 +495,7 @@ var init_indexes = __esm({
495
495
  });
496
496
 
497
497
  // src/utils/errors.ts
498
- var KnowledgeGraphError, EntityNotFoundError, RelationNotFoundError, ValidationError, CycleDetectedError, InvalidImportanceError, FileOperationError, InsufficientEntitiesError, RefConflictError, LowEntropyContentError, OperationCancelledError;
498
+ var KnowledgeGraphError, EntityNotFoundError, RelationNotFoundError, ValidationError, CycleDetectedError, InvalidImportanceError, FileOperationError, InsufficientEntitiesError, RefConflictError, VersionConflictError, LowEntropyContentError, OperationCancelledError;
499
499
  var init_errors = __esm({
500
500
  "src/utils/errors.ts"() {
501
501
  "use strict";
@@ -662,6 +662,31 @@ ${this.suggestions.map((s) => ` - ${s}`).join("\n")}`;
662
662
  this.name = "RefConflictError";
663
663
  }
664
664
  };
665
+ VersionConflictError = class extends KnowledgeGraphError {
666
+ entityName;
667
+ expected;
668
+ actual;
669
+ conflictingAgentId;
670
+ constructor(entityName, expected, actual, conflictingAgentId) {
671
+ super(
672
+ `Version conflict on entity '${entityName}': expected v${expected}, found v${actual}`,
673
+ "VERSION_CONFLICT",
674
+ {
675
+ context: { entityName, expected, actual, conflictingAgentId },
676
+ suggestions: [
677
+ "Re-fetch the entity to get the current version",
678
+ "Reconcile your changes with the live state",
679
+ "Retry the update with the new expectedVersion"
680
+ ]
681
+ }
682
+ );
683
+ this.name = "VersionConflictError";
684
+ this.entityName = entityName;
685
+ this.expected = expected;
686
+ this.actual = actual;
687
+ this.conflictingAgentId = conflictingAgentId;
688
+ }
689
+ };
665
690
  LowEntropyContentError = class extends Error {
666
691
  /** Stable error code for programmatic handling */
667
692
  code = "LOW_ENTROPY_CONTENT";
@@ -1006,7 +1031,8 @@ var init_schemas = __esm({
1006
1031
  lastModified: isoDateSchema.optional(),
1007
1032
  tags: z.array(tagSchema).optional(),
1008
1033
  importance: importanceSchema.optional(),
1009
- parentId: entityNameSchema.optional()
1034
+ parentId: entityNameSchema.optional(),
1035
+ contentHash: z.string().length(64).optional()
1010
1036
  }).strict();
1011
1037
  CreateEntitySchema = z.object({
1012
1038
  name: entityNameSchema,
@@ -1018,12 +1044,26 @@ var init_schemas = __esm({
1018
1044
  projectId: z.string().optional(),
1019
1045
  createdAt: isoDateSchema.optional(),
1020
1046
  lastModified: isoDateSchema.optional(),
1047
+ // v1.6.0: Freshness
1048
+ ttl: z.number().optional(),
1049
+ confidence: z.number().min(0).max(1).optional(),
1021
1050
  // v1.8.0: Memory versioning fields
1022
1051
  version: z.number().int().positive().optional(),
1023
1052
  parentEntityName: z.string().optional(),
1024
1053
  rootEntityName: z.string().optional(),
1025
1054
  isLatest: z.boolean().optional(),
1026
- supersededBy: z.string().optional()
1055
+ supersededBy: z.string().optional(),
1056
+ // v1.11.0: Memory Engine dedup
1057
+ contentHash: z.string().length(64).optional(),
1058
+ // η.4.4: Bitemporal validity
1059
+ validFrom: z.string().optional(),
1060
+ validUntil: z.string().optional(),
1061
+ observationMeta: z.array(z.object({
1062
+ content: z.string(),
1063
+ validFrom: z.string().optional(),
1064
+ validUntil: z.string().optional(),
1065
+ recordedAt: z.string().optional()
1066
+ })).optional()
1027
1067
  }).strict();
1028
1068
  UpdateEntitySchema = z.object({
1029
1069
  entityType: entityTypeSchema.optional(),
@@ -1033,7 +1073,21 @@ var init_schemas = __esm({
1033
1073
  parentId: entityNameSchema.optional(),
1034
1074
  // v1.8.0: Memory versioning fields
1035
1075
  isLatest: z.boolean().optional(),
1036
- supersededBy: z.string().optional()
1076
+ supersededBy: z.string().optional(),
1077
+ rootEntityName: entityNameSchema.optional(),
1078
+ parentEntityName: entityNameSchema.optional(),
1079
+ version: z.number().int().min(1).optional(),
1080
+ // v1.11.0: Memory Engine dedup
1081
+ contentHash: z.string().length(64).optional(),
1082
+ // η.4.4: Temporal Versioning expansion
1083
+ validFrom: z.string().optional(),
1084
+ validUntil: z.string().optional(),
1085
+ observationMeta: z.array(z.object({
1086
+ content: z.string(),
1087
+ validFrom: z.string().optional(),
1088
+ validUntil: z.string().optional(),
1089
+ recordedAt: z.string().optional()
1090
+ })).optional()
1037
1091
  }).strict();
1038
1092
  RelationSchema = z.object({
1039
1093
  from: entityNameSchema,
@@ -1107,7 +1161,18 @@ var init_schemas = __esm({
1107
1161
  entityType: entityTypeSchema.optional()
1108
1162
  }).strict();
1109
1163
  ImportFormatSchema = z.enum(["json", "csv", "graphml"]);
1110
- ExtendedExportFormatSchema = z.enum(["json", "csv", "graphml", "gexf", "dot", "markdown", "mermaid"]);
1164
+ ExtendedExportFormatSchema = z.enum([
1165
+ "json",
1166
+ "csv",
1167
+ "graphml",
1168
+ "gexf",
1169
+ "dot",
1170
+ "markdown",
1171
+ "mermaid",
1172
+ "turtle",
1173
+ "rdf-xml",
1174
+ "json-ld"
1175
+ ]);
1111
1176
  MergeStrategySchema = z.enum(["replace", "skip", "merge", "fail"]);
1112
1177
  ExportFilterSchema = z.object({
1113
1178
  startDate: isoDateSchema.optional(),
@@ -1251,17 +1316,20 @@ function escapeCsvFormula(field) {
1251
1316
  }
1252
1317
  return str;
1253
1318
  }
1254
- function validateFilePath(filePath, baseDir = process.cwd(), confineToBase = false) {
1255
- const normalized = path2.normalize(filePath);
1256
- const absolute = path2.isAbsolute(normalized) ? normalized : path2.join(baseDir, normalized);
1257
- const finalNormalized = path2.normalize(absolute);
1258
- const segments = finalNormalized.split(path2.sep);
1259
- if (segments.includes("..")) {
1319
+ function validateFilePath(filePath, baseDir = process.cwd(), confineToBase = true) {
1320
+ if (filePath.split(/[/\\]/).includes("..")) {
1260
1321
  throw new FileOperationError(
1261
1322
  `Path traversal detected in file path: ${filePath}`,
1262
1323
  filePath
1263
1324
  );
1264
1325
  }
1326
+ const finalNormalized = path2.resolve(baseDir, filePath);
1327
+ if (finalNormalized.split(path2.sep).includes("..")) {
1328
+ throw new FileOperationError(
1329
+ `Path traversal detected in file path after resolution: ${filePath}`,
1330
+ filePath
1331
+ );
1332
+ }
1265
1333
  if (confineToBase) {
1266
1334
  const resolvedBase = path2.resolve(baseDir) + path2.sep;
1267
1335
  if (!finalNormalized.startsWith(resolvedBase) && finalNormalized !== resolvedBase.slice(0, -1)) {
@@ -1831,7 +1899,7 @@ var init_StreamingExporter = __esm({
1831
1899
  * @throws {FileOperationError} If path traversal is detected
1832
1900
  */
1833
1901
  constructor(filePath) {
1834
- this.validatedFilePath = validateFilePath(filePath);
1902
+ this.validatedFilePath = validateFilePath(filePath, void 0, false);
1835
1903
  }
1836
1904
  /**
1837
1905
  * Get the validated file path.
@@ -2434,7 +2502,26 @@ var init_EntityManager = __esm({
2434
2502
  * }
2435
2503
  * ```
2436
2504
  */
2437
- async updateEntity(name, updates) {
2505
+ /**
2506
+ * Update an entity with optional optimistic-concurrency-control (η.5.5.c).
2507
+ *
2508
+ * Pass `options.expectedVersion` to enforce OCC: the caller asserts the
2509
+ * live entity has a specific `version`. If it differs (because another
2510
+ * agent / consolidation pass / contradiction-resolution incremented it
2511
+ * since the caller fetched), `VersionConflictError` is thrown with the
2512
+ * expected and actual versions. Omit `expectedVersion` for legacy
2513
+ * last-write-wins semantics (the default — backwards-compat).
2514
+ *
2515
+ * On a successful OCC-guarded write, `version` is auto-incremented:
2516
+ * `(entity.version ?? 1) + 1`. This makes OCC composable with the
2517
+ * existing v1.8.0 supersession-driven version increments.
2518
+ *
2519
+ * **Caveat**: a `ConsolidationScheduler` running in the background can
2520
+ * increment `version` between caller fetch and update, producing
2521
+ * spurious conflicts. Don't cache `expectedVersion` across scheduler
2522
+ * cycles — fetch immediately before writing.
2523
+ */
2524
+ async updateEntity(name, updates, options) {
2438
2525
  const validation = UpdateEntitySchema.safeParse(updates);
2439
2526
  if (!validation.success) {
2440
2527
  const errors = validation.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`);
@@ -2447,8 +2534,17 @@ var init_EntityManager = __esm({
2447
2534
  if (!entity) {
2448
2535
  throw new EntityNotFoundError(name);
2449
2536
  }
2537
+ if (options?.expectedVersion !== void 0) {
2538
+ const liveVersion = entity.version ?? 1;
2539
+ if (liveVersion !== options.expectedVersion) {
2540
+ throw new VersionConflictError(name, options.expectedVersion, liveVersion);
2541
+ }
2542
+ }
2450
2543
  Object.assign(entity, sanitizeObject(updates));
2451
2544
  entity.lastModified = (/* @__PURE__ */ new Date()).toISOString();
2545
+ if (options?.expectedVersion !== void 0) {
2546
+ entity.version = (entity.version ?? 1) + 1;
2547
+ }
2452
2548
  await this.storage.saveGraph(graph);
2453
2549
  return entity;
2454
2550
  } finally {
@@ -2708,6 +2804,85 @@ var init_EntityManager = __esm({
2708
2804
  release();
2709
2805
  }
2710
2806
  }
2807
+ // ==================== η.4.4: Temporal Versioning ====================
2808
+ //
2809
+ // Mirrors the v1.9.0 RelationManager surface (invalidateRelation /
2810
+ // queryAsOf / timeline) for entities. Orthogonal to v1.8.0 supersession
2811
+ // (`version`/`supersededBy`): supersession answers "which version is
2812
+ // current?", temporal validity answers "was the entity true at time T?".
2813
+ // An entity may be superseded but still valid at a past asOf date, and
2814
+ // vice versa.
2815
+ /**
2816
+ * Mark an entity as no longer valid by setting `validUntil`. Idempotent:
2817
+ * a second call updates the existing `validUntil`. Does not delete the
2818
+ * entity — `entityAsOf` still returns it for past asOf timestamps.
2819
+ *
2820
+ * @param name - The entity to invalidate
2821
+ * @param ended - ISO 8601 timestamp; defaults to current time
2822
+ * @throws {EntityNotFoundError} If no entity exists with the given name
2823
+ */
2824
+ async invalidateEntity(name, ended) {
2825
+ const release = await this.storage.graphMutex.acquire();
2826
+ try {
2827
+ const graph = await this.storage.getGraphForMutation();
2828
+ const entity = graph.entities.find((e) => e.name === name);
2829
+ if (!entity) throw new EntityNotFoundError(name);
2830
+ entity.validUntil = ended ?? (/* @__PURE__ */ new Date()).toISOString();
2831
+ entity.lastModified = (/* @__PURE__ */ new Date()).toISOString();
2832
+ await this.storage.saveGraph(graph);
2833
+ } finally {
2834
+ release();
2835
+ }
2836
+ }
2837
+ /**
2838
+ * Return the entity at a given point in time, or null if it didn't exist
2839
+ * (or was already invalidated) then. An entity is valid at `asOf` when:
2840
+ * - `validFrom` is undefined OR `validFrom` <= asOf
2841
+ * - `validUntil` is undefined OR `validUntil` >= asOf
2842
+ *
2843
+ * @param name - The entity name
2844
+ * @param asOf - ISO 8601 date string
2845
+ * @throws {ValidationError} If `asOf` is not an ISO 8601 date string
2846
+ */
2847
+ async entityAsOf(name, asOf) {
2848
+ if (!/^\d{4}-\d{2}-\d{2}/.test(asOf)) {
2849
+ throw new ValidationError(`asOf must be an ISO 8601 date string, got: '${asOf}'`, []);
2850
+ }
2851
+ const graph = await this.storage.loadGraph();
2852
+ const entity = graph.entities.find((e) => e.name === name);
2853
+ if (!entity) return null;
2854
+ const vf = entity.validFrom;
2855
+ const vu = entity.validUntil;
2856
+ if (vf && vf > asOf) return null;
2857
+ if (vu && vu < asOf) return null;
2858
+ return entity;
2859
+ }
2860
+ /**
2861
+ * Return all temporal versions of an entity in chronological order
2862
+ * (by `validFrom`, with unbounded entities last). When `name` matches
2863
+ * a member of a v1.8.0 supersession chain, returns the full chain
2864
+ * sorted by `validFrom`. Otherwise returns just the named entity (or []).
2865
+ *
2866
+ * @param name - Any entity name in the chain
2867
+ */
2868
+ async entityTimeline(name) {
2869
+ const graph = await this.storage.loadGraph();
2870
+ const entity = graph.entities.find((e) => e.name === name);
2871
+ if (!entity) return [];
2872
+ const rootName = entity.rootEntityName ?? entity.name;
2873
+ const chain = graph.entities.filter(
2874
+ (e) => (e.rootEntityName ?? e.name) === rootName
2875
+ );
2876
+ chain.sort((a, b) => {
2877
+ const aFrom = a.validFrom ?? "";
2878
+ const bFrom = b.validFrom ?? "";
2879
+ if (!aFrom && !bFrom) return 0;
2880
+ if (!aFrom) return 1;
2881
+ if (!bFrom) return -1;
2882
+ return aFrom.localeCompare(bFrom);
2883
+ });
2884
+ return chain;
2885
+ }
2711
2886
  };
2712
2887
  }
2713
2888
  });
@@ -2752,10 +2927,177 @@ var init_IOManager = __esm({
2752
2927
  return this.exportAsMarkdown(graph);
2753
2928
  case "mermaid":
2754
2929
  return this.exportAsMermaid(graph);
2930
+ case "turtle":
2931
+ return this.exportAsTurtle(graph);
2932
+ case "rdf-xml":
2933
+ return this.exportAsRdfXml(graph);
2934
+ case "json-ld":
2935
+ return this.exportAsJsonLd(graph);
2755
2936
  default:
2756
2937
  throw new Error(`Unsupported export format: ${format}`);
2757
2938
  }
2758
2939
  }
2940
+ // -------- η.5.4 Standards Compliance — RDF / Turtle / JSON-LD --------
2941
+ /** IRI for an entity resource. Format: `urn:memoryjs:entity:<percent-encoded-name>`. */
2942
+ entityIri(name) {
2943
+ return `urn:memoryjs:entity:${encodeURIComponent(name)}`;
2944
+ }
2945
+ /** IRI for a relation predicate. Format: `urn:memoryjs:rel:<percent-encoded-type>`. */
2946
+ relationIri(type) {
2947
+ return `urn:memoryjs:rel:${encodeURIComponent(type)}`;
2948
+ }
2949
+ /**
2950
+ * Escape a string for a Turtle `STRING_LITERAL_QUOTE` per W3C Turtle 1.1.
2951
+ * - Named ECHAR escapes for `\\ " \n \r \t \b \f`
2952
+ * - Other C0 control chars (forbidden unescaped in `"..."`) as `\uXXXX`
2953
+ */
2954
+ turtleEscape(s) {
2955
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\x08/g, "\\b").replace(/\x0c/g, "\\f").replace(
2956
+ /[\x00-\x07\x0B\x0E-\x1F]/g,
2957
+ (c) => `\\u${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`
2958
+ );
2959
+ }
2960
+ /**
2961
+ * Test whether a string is a valid XML 1.0 NCName — ASCII subset,
2962
+ * sufficient because `relationIri()` percent-encodes everything else.
2963
+ * RDF/XML requires this for property-element predicate names.
2964
+ */
2965
+ isValidNCName(s) {
2966
+ return /^[A-Za-z_][A-Za-z0-9_\-.]*$/.test(s);
2967
+ }
2968
+ /**
2969
+ * Export as Turtle (W3C RDF 1.1).
2970
+ * - entity → `urn:memoryjs:entity:<name>` resource
2971
+ * - entityType → `rdf:type` with `urn:memoryjs:type:<type>` IRI
2972
+ * - observations → `rdfs:comment` literals
2973
+ * - tags → `dcterms:subject` literals
2974
+ * - createdAt → `dcterms:created` literal
2975
+ * - relation → `<from> <urn:memoryjs:rel:<type>> <to>` triple
2976
+ */
2977
+ exportAsTurtle(graph) {
2978
+ const lines = [];
2979
+ lines.push("@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .");
2980
+ lines.push("@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .");
2981
+ lines.push("@prefix dcterms: <http://purl.org/dc/terms/> .");
2982
+ lines.push("");
2983
+ for (const entity of graph.entities) {
2984
+ const subject = `<${this.entityIri(entity.name)}>`;
2985
+ lines.push(`${subject} a <urn:memoryjs:type:${encodeURIComponent(entity.entityType)}> ;`);
2986
+ lines.push(` rdfs:label "${this.turtleEscape(entity.name)}" ;`);
2987
+ for (const obs of entity.observations) {
2988
+ lines.push(` rdfs:comment "${this.turtleEscape(obs)}" ;`);
2989
+ }
2990
+ for (const tag of entity.tags ?? []) {
2991
+ lines.push(` dcterms:subject "${this.turtleEscape(tag)}" ;`);
2992
+ }
2993
+ if (entity.createdAt) {
2994
+ lines.push(` dcterms:created "${this.turtleEscape(entity.createdAt)}" ;`);
2995
+ }
2996
+ const last = lines.length - 1;
2997
+ lines[last] = lines[last].replace(/ ;$/, " .");
2998
+ }
2999
+ if (graph.entities.length > 0) lines.push("");
3000
+ for (const relation of graph.relations) {
3001
+ const subject = `<${this.entityIri(relation.from)}>`;
3002
+ const predicate = `<${this.relationIri(relation.relationType)}>`;
3003
+ const object = `<${this.entityIri(relation.to)}>`;
3004
+ lines.push(`${subject} ${predicate} ${object} .`);
3005
+ }
3006
+ return lines.join("\n");
3007
+ }
3008
+ /**
3009
+ * Export as RDF/XML (W3C RDF 1.1 XML serialization).
3010
+ * - Same triple set as Turtle, in XML form
3011
+ * - NCName-valid relation types → property-element under `mjsRel:`
3012
+ * - Otherwise → asserted `mjsRel:link` triple plus `rdf:Statement` reification preserving the original predicate IRI
3013
+ */
3014
+ exportAsRdfXml(graph) {
3015
+ const xmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3016
+ const lines = [];
3017
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
3018
+ lines.push("<rdf:RDF");
3019
+ lines.push(' xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"');
3020
+ lines.push(' xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"');
3021
+ lines.push(' xmlns:dcterms="http://purl.org/dc/terms/"');
3022
+ lines.push(' xmlns:mjsRel="urn:memoryjs:rel:">');
3023
+ for (const entity of graph.entities) {
3024
+ lines.push(` <rdf:Description rdf:about="${xmlEscape(this.entityIri(entity.name))}">`);
3025
+ lines.push(` <rdf:type rdf:resource="urn:memoryjs:type:${xmlEscape(encodeURIComponent(entity.entityType))}"/>`);
3026
+ lines.push(` <rdfs:label>${xmlEscape(entity.name)}</rdfs:label>`);
3027
+ for (const obs of entity.observations) {
3028
+ lines.push(` <rdfs:comment>${xmlEscape(obs)}</rdfs:comment>`);
3029
+ }
3030
+ for (const tag of entity.tags ?? []) {
3031
+ lines.push(` <dcterms:subject>${xmlEscape(tag)}</dcterms:subject>`);
3032
+ }
3033
+ lines.push(" </rdf:Description>");
3034
+ }
3035
+ for (const relation of graph.relations) {
3036
+ const fromIri = xmlEscape(this.entityIri(relation.from));
3037
+ const toIri = xmlEscape(this.entityIri(relation.to));
3038
+ if (this.isValidNCName(relation.relationType)) {
3039
+ lines.push(` <rdf:Description rdf:about="${fromIri}">`);
3040
+ lines.push(` <mjsRel:${xmlEscape(relation.relationType)} rdf:resource="${toIri}"/>`);
3041
+ lines.push(" </rdf:Description>");
3042
+ continue;
3043
+ }
3044
+ const predIri = xmlEscape(this.relationIri(relation.relationType));
3045
+ lines.push(` <rdf:Description rdf:about="${fromIri}">`);
3046
+ lines.push(` <mjsRel:link rdf:resource="${toIri}"/>`);
3047
+ lines.push(" </rdf:Description>");
3048
+ lines.push(" <rdf:Description>");
3049
+ lines.push(' <rdf:type rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement"/>');
3050
+ lines.push(` <rdf:subject rdf:resource="${fromIri}"/>`);
3051
+ lines.push(` <rdf:predicate rdf:resource="${predIri}"/>`);
3052
+ lines.push(` <rdf:object rdf:resource="${toIri}"/>`);
3053
+ lines.push(" </rdf:Description>");
3054
+ }
3055
+ lines.push("</rdf:RDF>");
3056
+ return lines.join("\n");
3057
+ }
3058
+ /**
3059
+ * Export as JSON-LD (JSON for Linking Data).
3060
+ * - `@context` maps memoryjs schema to RDFS + DCTerms vocabularies
3061
+ * - observations/tags use `@container: @set` so each value becomes its own triple (matches Turtle/RDF-XML), not an `rdf:List`
3062
+ * - any JSON-LD parser yields the same RDF graph as the Turtle export
3063
+ */
3064
+ exportAsJsonLd(graph) {
3065
+ const context = {
3066
+ "@vocab": "urn:memoryjs:",
3067
+ rdfs: "http://www.w3.org/2000/01/rdf-schema#",
3068
+ dcterms: "http://purl.org/dc/terms/",
3069
+ name: "rdfs:label",
3070
+ entityType: "@type",
3071
+ observations: { "@id": "rdfs:comment", "@container": "@set" },
3072
+ tags: { "@id": "dcterms:subject", "@container": "@set" },
3073
+ createdAt: "dcterms:created",
3074
+ lastModified: "dcterms:modified",
3075
+ from: { "@id": "urn:memoryjs:rel:from", "@type": "@id" },
3076
+ to: { "@id": "urn:memoryjs:rel:to", "@type": "@id" },
3077
+ relationType: "urn:memoryjs:rel:type"
3078
+ };
3079
+ const doc = {
3080
+ "@context": context,
3081
+ "@graph": [
3082
+ ...graph.entities.map((entity) => ({
3083
+ "@id": this.entityIri(entity.name),
3084
+ name: entity.name,
3085
+ entityType: `urn:memoryjs:type:${encodeURIComponent(entity.entityType)}`,
3086
+ observations: entity.observations,
3087
+ ...entity.tags && entity.tags.length > 0 ? { tags: entity.tags } : {},
3088
+ ...entity.createdAt ? { createdAt: entity.createdAt } : {},
3089
+ ...entity.lastModified ? { lastModified: entity.lastModified } : {}
3090
+ })),
3091
+ ...graph.relations.map((relation) => ({
3092
+ "@id": `urn:memoryjs:relation:${encodeURIComponent(relation.from)}:${encodeURIComponent(relation.relationType)}:${encodeURIComponent(relation.to)}`,
3093
+ from: this.entityIri(relation.from),
3094
+ to: this.entityIri(relation.to),
3095
+ relationType: relation.relationType
3096
+ }))
3097
+ ]
3098
+ };
3099
+ return JSON.stringify(doc, null, 2);
3100
+ }
2759
3101
  /** Export graph with optional brotli compression. */
2760
3102
  async exportGraphWithCompression(graph, format, options) {
2761
3103
  const shouldStream = options?.streaming || options?.outputPath && graph.entities.length >= STREAMING_CONFIG.STREAMING_THRESHOLD;
@@ -2797,7 +3139,7 @@ var init_IOManager = __esm({
2797
3139
  }
2798
3140
  /** Stream export to a file for large graphs. */
2799
3141
  async streamExport(format, graph, options) {
2800
- const validatedOutputPath = validateFilePath(options.outputPath);
3142
+ const validatedOutputPath = validateFilePath(options.outputPath, void 0, false);
2801
3143
  const exporter = new StreamingExporter(validatedOutputPath);
2802
3144
  let result;
2803
3145
  switch (format) {
@@ -3568,7 +3910,10 @@ var init_IOManager = __esm({
3568
3910
  async restoreFromBackup(backupPath) {
3569
3911
  try {
3570
3912
  validateFilePath(backupPath, this.backupDir, true);
3571
- await fs.access(backupPath);
3913
+ const stat = await fs.lstat(backupPath);
3914
+ if (stat.isSymbolicLink()) {
3915
+ throw new FileOperationError("Symbolic links are not allowed for backup restore", backupPath);
3916
+ }
3572
3917
  const isCompressed = hasBrotliExtension(backupPath);
3573
3918
  const backupBuffer = await fs.readFile(backupPath);
3574
3919
  let backupContent;
@@ -3598,7 +3943,8 @@ var init_IOManager = __esm({
3598
3943
  validateFilePath(backupPath, this.backupDir, true);
3599
3944
  await fs.unlink(backupPath);
3600
3945
  try {
3601
- const metaPath = join(dirname(backupPath), `${backupPath.split(/[/\\]/).pop()}.meta.json`);
3946
+ const baseName = backupPath.split(/[/\\]/).pop() ?? "";
3947
+ const metaPath = join(this.backupDir, `${baseName}.meta.json`);
3602
3948
  validateFilePath(metaPath, this.backupDir, true);
3603
3949
  await fs.unlink(metaPath);
3604
3950
  } catch {
@@ -3649,10 +3995,10 @@ var init_IOManager = __esm({
3649
3995
  skippedDuplicates: 0,
3650
3996
  entityNames: []
3651
3997
  };
3652
- const { createHash } = await import("crypto");
3998
+ const { createHash: createHash2 } = await import("crypto");
3653
3999
  const graph = await this.storage.loadGraph();
3654
4000
  const existingObsSet = new Set(
3655
- graph.entities.map((e) => createHash("sha256").update(e.observations.join("\n")).digest("hex"))
4001
+ graph.entities.map((e) => createHash2("sha256").update(e.observations.join("\n")).digest("hex"))
3656
4002
  );
3657
4003
  const { EntityManager: EntityManager2 } = await Promise.resolve().then(() => (init_EntityManager(), EntityManager_exports));
3658
4004
  const em = new EntityManager2(this.storage);
@@ -3663,7 +4009,7 @@ var init_IOManager = __esm({
3663
4009
  const chunk = chunks[i];
3664
4010
  const entityName = `${source}-${String(i + 1).padStart(3, "0")}`;
3665
4011
  const observations = chunk.map((m) => `[${m.role}] ${m.content}`);
3666
- const obsKey = createHash("sha256").update(observations.join("\n")).digest("hex");
4012
+ const obsKey = createHash2("sha256").update(observations.join("\n")).digest("hex");
3667
4013
  if (existingObsSet.has(obsKey)) {
3668
4014
  result.skippedDuplicates++;
3669
4015
  continue;
@@ -3741,19 +4087,24 @@ var init_IOManager = __esm({
3741
4087
  /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/m
3742
4088
  ];
3743
4089
  let rawSessions = [content];
4090
+ const MAX_SPLIT_LENGTH = 10 * 1024 * 1024;
4091
+ const splitContent = content.length > MAX_SPLIT_LENGTH ? content.slice(0, MAX_SPLIT_LENGTH) : content;
3744
4092
  for (const delimiter of delimiters) {
3745
- const globalDelimiter = new RegExp(delimiter.source, delimiter.flags.includes("g") ? delimiter.flags : delimiter.flags + "g");
3746
- if (globalDelimiter.test(content)) {
3747
- const lookaheadDelimiter = new RegExp(`(?=${delimiter.source})`, delimiter.flags.replace("g", "").replace("m", "") + "m");
3748
- const parts = content.split(lookaheadDelimiter);
4093
+ const flags = delimiter.flags.includes("g") ? delimiter.flags : delimiter.flags + "g";
4094
+ const globalDelimiter = new RegExp(delimiter.source, flags);
4095
+ if (globalDelimiter.test(splitContent)) {
4096
+ const lookaheadFlags = delimiter.flags.replace(/[gm]/g, "") + "m";
4097
+ const lookaheadDelimiter = new RegExp(`(?=${delimiter.source})`, lookaheadFlags);
4098
+ const parts = splitContent.split(lookaheadDelimiter);
4099
+ const MAX_PARTS = 1e4;
3749
4100
  if (parts.length > 1) {
3750
- rawSessions = parts;
4101
+ rawSessions = parts.slice(0, MAX_PARTS);
3751
4102
  break;
3752
4103
  }
3753
- const splitDelimiter = new RegExp(delimiter.source, delimiter.flags.includes("g") ? delimiter.flags : delimiter.flags + "g");
3754
- const splitParts = content.split(splitDelimiter);
4104
+ const splitDelimiter = new RegExp(delimiter.source, flags);
4105
+ const splitParts = splitContent.split(splitDelimiter);
3755
4106
  if (splitParts.length > 1) {
3756
- rawSessions = splitParts;
4107
+ rawSessions = splitParts.slice(0, MAX_PARTS);
3757
4108
  break;
3758
4109
  }
3759
4110
  }
@@ -4606,8 +4957,15 @@ var init_GraphEventEmitter = __esm({
4606
4957
 
4607
4958
  // src/core/GraphStorage.ts
4608
4959
  import { promises as fs2 } from "fs";
4960
+ import { randomBytes } from "crypto";
4609
4961
  import { Mutex } from "async-mutex";
4610
- var GraphStorage;
4962
+ function copyOptionalPersistedFields(src, dst) {
4963
+ for (const field of OPTIONAL_PERSISTED_ENTITY_FIELDS) {
4964
+ const v = src[field];
4965
+ if (v !== void 0) dst[field] = v;
4966
+ }
4967
+ }
4968
+ var OPTIONAL_PERSISTED_ENTITY_FIELDS, GraphStorage;
4611
4969
  var init_GraphStorage = __esm({
4612
4970
  "src/core/GraphStorage.ts"() {
4613
4971
  "use strict";
@@ -4617,6 +4975,60 @@ var init_GraphStorage = __esm({
4617
4975
  init_utils();
4618
4976
  init_TransactionManager();
4619
4977
  init_GraphEventEmitter();
4978
+ OPTIONAL_PERSISTED_ENTITY_FIELDS = [
4979
+ // Core Entity (types/types.ts)
4980
+ "tags",
4981
+ "importance",
4982
+ "parentId",
4983
+ "projectId",
4984
+ "version",
4985
+ "parentEntityName",
4986
+ "rootEntityName",
4987
+ "isLatest",
4988
+ "supersededBy",
4989
+ "contentHash",
4990
+ "ttl",
4991
+ "confidence",
4992
+ // η.4.4 temporal versioning expansion
4993
+ "validFrom",
4994
+ "validUntil",
4995
+ "observationMeta",
4996
+ // AgentEntity extension (types/agent-memory.ts)
4997
+ "memoryType",
4998
+ "sessionId",
4999
+ "conversationId",
5000
+ "taskId",
5001
+ "expiresAt",
5002
+ "isWorkingMemory",
5003
+ "promotedAt",
5004
+ "promotedFrom",
5005
+ "markedForPromotion",
5006
+ "accessCount",
5007
+ "lastAccessedAt",
5008
+ "accessPattern",
5009
+ "confirmationCount",
5010
+ "decayRate",
5011
+ "agentId",
5012
+ "visibility",
5013
+ "source",
5014
+ // SessionEntity extension (types/agent-memory.ts)
5015
+ "startedAt",
5016
+ "endedAt",
5017
+ "status",
5018
+ "goalDescription",
5019
+ "taskType",
5020
+ "userIntent",
5021
+ "memoryCount",
5022
+ "consolidatedCount",
5023
+ "previousSessionId",
5024
+ "relatedSessionIds",
5025
+ "outcome",
5026
+ "failureCauses",
5027
+ // ArtifactEntity extension (types/artifact.ts)
5028
+ "artifactType",
5029
+ "toolName",
5030
+ "shortId"
5031
+ ];
4620
5032
  GraphStorage = class {
4621
5033
  /**
4622
5034
  * Mutex for thread-safe access to storage operations.
@@ -4687,7 +5099,7 @@ var init_GraphStorage = __esm({
4687
5099
  * @throws {FileOperationError} If path traversal is detected
4688
5100
  */
4689
5101
  constructor(memoryFilePath) {
4690
- this.memoryFilePath = validateFilePath(memoryFilePath);
5102
+ this.memoryFilePath = validateFilePath(memoryFilePath, void 0, false);
4691
5103
  }
4692
5104
  // ==================== Phase 10 Sprint 2: Event Emitter Access ====================
4693
5105
  /**
@@ -4713,6 +5125,25 @@ var init_GraphStorage = __esm({
4713
5125
  get events() {
4714
5126
  return this.eventEmitter;
4715
5127
  }
5128
+ /**
5129
+ * Synchronous access to the in-memory cached graph. Returns `null` if the
5130
+ * cache is not yet warm — in which case consumers should call
5131
+ * `loadGraph()` once to populate it and then use this accessor on
5132
+ * subsequent reads.
5133
+ *
5134
+ * Intended for integrations that need a synchronous read path backed by
5135
+ * `GraphStorage`'s already-materialized cache (e.g., the `ObservableDataModel`
5136
+ * adapter consumed by JSON-UI's `DataProvider`, which must supply a
5137
+ * synchronous `snapshot()` to React's `useSyncExternalStore`). Most
5138
+ * callers should prefer `loadGraph()`, which lazy-loads on first call.
5139
+ *
5140
+ * The returned reference is the live cache object — do NOT mutate it.
5141
+ * Use `loadGraph()` for a defensive read or `getGraphForMutation()` for
5142
+ * a mutable copy.
5143
+ */
5144
+ get cachedGraph() {
5145
+ return this.cache;
5146
+ }
4716
5147
  // ==================== Durable File Operations ====================
4717
5148
  /**
4718
5149
  * Write content to file with fsync for durability.
@@ -4720,7 +5151,7 @@ var init_GraphStorage = __esm({
4720
5151
  * @param content - Content to write
4721
5152
  */
4722
5153
  async durableWriteFile(content) {
4723
- const tmpPath = `${this.memoryFilePath}.tmp.${process.pid}`;
5154
+ const tmpPath = `${this.memoryFilePath}.tmp.${process.pid}.${randomBytes(6).toString("hex")}`;
4724
5155
  const fd = await fs2.open(tmpPath, "w");
4725
5156
  try {
4726
5157
  await fd.write(content);
@@ -4913,15 +5344,7 @@ var init_GraphStorage = __esm({
4913
5344
  createdAt: entity.createdAt,
4914
5345
  lastModified: entity.lastModified
4915
5346
  };
4916
- if (entity.tags !== void 0) entityData.tags = entity.tags;
4917
- if (entity.importance !== void 0) entityData.importance = entity.importance;
4918
- if (entity.parentId !== void 0) entityData.parentId = entity.parentId;
4919
- if (entity.projectId !== void 0) entityData.projectId = entity.projectId;
4920
- if (entity.version !== void 0) entityData.version = entity.version;
4921
- if (entity.parentEntityName !== void 0) entityData.parentEntityName = entity.parentEntityName;
4922
- if (entity.rootEntityName !== void 0) entityData.rootEntityName = entity.rootEntityName;
4923
- if (entity.isLatest !== void 0) entityData.isLatest = entity.isLatest;
4924
- if (entity.supersededBy !== void 0) entityData.supersededBy = entity.supersededBy;
5347
+ copyOptionalPersistedFields(entity, entityData);
4925
5348
  const line = JSON.stringify(entityData);
4926
5349
  try {
4927
5350
  const stat = await fs2.stat(this.memoryFilePath);
@@ -5035,15 +5458,7 @@ var init_GraphStorage = __esm({
5035
5458
  createdAt: e.createdAt,
5036
5459
  lastModified: e.lastModified
5037
5460
  };
5038
- if (e.tags !== void 0) entityData.tags = e.tags;
5039
- if (e.importance !== void 0) entityData.importance = e.importance;
5040
- if (e.parentId !== void 0) entityData.parentId = e.parentId;
5041
- if (e.projectId !== void 0) entityData.projectId = e.projectId;
5042
- if (e.version !== void 0) entityData.version = e.version;
5043
- if (e.parentEntityName !== void 0) entityData.parentEntityName = e.parentEntityName;
5044
- if (e.rootEntityName !== void 0) entityData.rootEntityName = e.rootEntityName;
5045
- if (e.isLatest !== void 0) entityData.isLatest = e.isLatest;
5046
- if (e.supersededBy !== void 0) entityData.supersededBy = e.supersededBy;
5461
+ copyOptionalPersistedFields(e, entityData);
5047
5462
  return JSON.stringify(entityData);
5048
5463
  }),
5049
5464
  // Serialize relations with metadata (Phase 1 Sprint 5)
@@ -5121,15 +5536,7 @@ var init_GraphStorage = __esm({
5121
5536
  createdAt: updatedEntity.createdAt,
5122
5537
  lastModified: updatedEntity.lastModified
5123
5538
  };
5124
- if (updatedEntity.tags !== void 0) entityData.tags = updatedEntity.tags;
5125
- if (updatedEntity.importance !== void 0) entityData.importance = updatedEntity.importance;
5126
- if (updatedEntity.parentId !== void 0) entityData.parentId = updatedEntity.parentId;
5127
- if (updatedEntity.projectId !== void 0) entityData.projectId = updatedEntity.projectId;
5128
- if (updatedEntity.version !== void 0) entityData.version = updatedEntity.version;
5129
- if (updatedEntity.parentEntityName !== void 0) entityData.parentEntityName = updatedEntity.parentEntityName;
5130
- if (updatedEntity.rootEntityName !== void 0) entityData.rootEntityName = updatedEntity.rootEntityName;
5131
- if (updatedEntity.isLatest !== void 0) entityData.isLatest = updatedEntity.isLatest;
5132
- if (updatedEntity.supersededBy !== void 0) entityData.supersededBy = updatedEntity.supersededBy;
5539
+ copyOptionalPersistedFields(updatedEntity, entityData);
5133
5540
  const line = JSON.stringify(entityData);
5134
5541
  try {
5135
5542
  const stat = await fs2.stat(this.memoryFilePath);
@@ -5370,7 +5777,7 @@ var init_SQLiteStorage = __esm({
5370
5777
  init_searchCache();
5371
5778
  init_indexes();
5372
5779
  init_utils();
5373
- SQLiteStorage = class {
5780
+ SQLiteStorage = class _SQLiteStorage {
5374
5781
  /**
5375
5782
  * Mutex for thread-safe access to storage operations.
5376
5783
  * Prevents concurrent writes from corrupting the cache.
@@ -5424,7 +5831,7 @@ var init_SQLiteStorage = __esm({
5424
5831
  * @throws {FileOperationError} If path traversal is detected
5425
5832
  */
5426
5833
  constructor(dbFilePath) {
5427
- this.validatedDbFilePath = validateFilePath(dbFilePath);
5834
+ this.validatedDbFilePath = validateFilePath(dbFilePath, void 0, false);
5428
5835
  }
5429
5836
  /**
5430
5837
  * Initialize the database connection and schema.
@@ -5458,7 +5865,9 @@ var init_SQLiteStorage = __esm({
5458
5865
  parentEntityName TEXT,
5459
5866
  rootEntityName TEXT,
5460
5867
  isLatest INTEGER DEFAULT 1,
5461
- supersededBy TEXT
5868
+ supersededBy TEXT,
5869
+ contentHash TEXT,
5870
+ agentMetadata TEXT
5462
5871
  )
5463
5872
  `);
5464
5873
  this.db.exec(`
@@ -5568,8 +5977,87 @@ var init_SQLiteStorage = __esm({
5568
5977
  if (!columnNames.has("supersededBy")) {
5569
5978
  this.db.exec("ALTER TABLE entities ADD COLUMN supersededBy TEXT");
5570
5979
  }
5980
+ if (!columnNames.has("contentHash")) {
5981
+ this.db.exec("ALTER TABLE entities ADD COLUMN contentHash TEXT");
5982
+ }
5983
+ if (!columnNames.has("agentMetadata")) {
5984
+ this.db.exec("ALTER TABLE entities ADD COLUMN agentMetadata TEXT");
5985
+ }
5571
5986
  this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_projectId ON entities(projectId)`);
5572
5987
  this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_isLatest ON entities(isLatest)`);
5988
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_content_hash ON entities(contentHash)`);
5989
+ }
5990
+ /**
5991
+ * Fields that round-trip through the `agentMetadata` JSON blob column.
5992
+ * Native columns (importance, projectId, version, contentHash, etc.) are
5993
+ * stored separately for SQL-side queryability. Everything else lives in
5994
+ * the blob to avoid schema-migration drift as the type system evolves.
5995
+ * Mirrors `OPTIONAL_PERSISTED_ENTITY_FIELDS` in GraphStorage.ts minus
5996
+ * the fields that already have native columns.
5997
+ */
5998
+ static EXTENSION_FIELDS = [
5999
+ // Core Entity (not in native columns)
6000
+ "ttl",
6001
+ "confidence",
6002
+ // η.4.4 temporal versioning expansion
6003
+ "validFrom",
6004
+ "validUntil",
6005
+ "observationMeta",
6006
+ // AgentEntity (types/agent-memory.ts)
6007
+ "memoryType",
6008
+ "sessionId",
6009
+ "conversationId",
6010
+ "taskId",
6011
+ "expiresAt",
6012
+ "isWorkingMemory",
6013
+ "promotedAt",
6014
+ "promotedFrom",
6015
+ "markedForPromotion",
6016
+ "accessCount",
6017
+ "lastAccessedAt",
6018
+ "accessPattern",
6019
+ "confirmationCount",
6020
+ "decayRate",
6021
+ "agentId",
6022
+ "visibility",
6023
+ "source",
6024
+ // SessionEntity (types/agent-memory.ts)
6025
+ "startedAt",
6026
+ "endedAt",
6027
+ "status",
6028
+ "goalDescription",
6029
+ "taskType",
6030
+ "userIntent",
6031
+ "memoryCount",
6032
+ "consolidatedCount",
6033
+ "previousSessionId",
6034
+ "relatedSessionIds",
6035
+ "outcome",
6036
+ "failureCauses",
6037
+ // ArtifactEntity (types/artifact.ts)
6038
+ "artifactType",
6039
+ "toolName",
6040
+ "shortId"
6041
+ ];
6042
+ /** Build the JSON blob payload for the `agentMetadata` column. */
6043
+ serializeExtensionFields(entity) {
6044
+ const src = entity;
6045
+ const out = {};
6046
+ for (const field of _SQLiteStorage.EXTENSION_FIELDS) {
6047
+ const v = src[field];
6048
+ if (v !== void 0) out[field] = v;
6049
+ }
6050
+ return Object.keys(out).length === 0 ? null : JSON.stringify(out);
6051
+ }
6052
+ /** Inverse of `serializeExtensionFields`. Tolerant of malformed JSON. */
6053
+ parseExtensionFields(blob) {
6054
+ if (!blob) return {};
6055
+ try {
6056
+ const parsed = JSON.parse(blob);
6057
+ return parsed && typeof parsed === "object" ? parsed : {};
6058
+ } catch {
6059
+ return {};
6060
+ }
5573
6061
  }
5574
6062
  /**
5575
6063
  * Load all data from SQLite into memory cache.
@@ -5596,11 +6084,23 @@ var init_SQLiteStorage = __esm({
5596
6084
  * Convert a database row to an Entity object.
5597
6085
  */
5598
6086
  rowToEntity(row) {
6087
+ let observations;
6088
+ let tags;
6089
+ try {
6090
+ observations = JSON.parse(row.observations);
6091
+ } catch {
6092
+ observations = [];
6093
+ }
6094
+ try {
6095
+ tags = row.tags ? JSON.parse(row.tags) : void 0;
6096
+ } catch {
6097
+ tags = void 0;
6098
+ }
5599
6099
  const entity = {
5600
6100
  name: row.name,
5601
6101
  entityType: row.entityType,
5602
- observations: JSON.parse(row.observations),
5603
- tags: row.tags ? JSON.parse(row.tags) : void 0,
6102
+ observations,
6103
+ tags,
5604
6104
  importance: row.importance ?? void 0,
5605
6105
  parentId: row.parentId ?? void 0,
5606
6106
  createdAt: row.createdAt,
@@ -5612,6 +6112,9 @@ var init_SQLiteStorage = __esm({
5612
6112
  if (row.rootEntityName != null) entity.rootEntityName = row.rootEntityName;
5613
6113
  if (row.isLatest != null) entity.isLatest = row.isLatest === 1;
5614
6114
  if (row.supersededBy != null) entity.supersededBy = row.supersededBy;
6115
+ if (row.contentHash != null) entity.contentHash = row.contentHash;
6116
+ const ext = this.parseExtensionFields(row.agentMetadata ?? null);
6117
+ Object.assign(entity, ext);
5615
6118
  return entity;
5616
6119
  }
5617
6120
  /**
@@ -5628,8 +6131,18 @@ var init_SQLiteStorage = __esm({
5628
6131
  };
5629
6132
  if (row.weight !== null) relation.weight = row.weight;
5630
6133
  if (row.confidence !== null) relation.confidence = row.confidence;
5631
- if (row.properties !== null) relation.properties = JSON.parse(row.properties);
5632
- if (row.metadata !== null) relation.metadata = JSON.parse(row.metadata);
6134
+ if (row.properties !== null) {
6135
+ try {
6136
+ relation.properties = JSON.parse(row.properties);
6137
+ } catch {
6138
+ }
6139
+ }
6140
+ if (row.metadata !== null) {
6141
+ try {
6142
+ relation.metadata = JSON.parse(row.metadata);
6143
+ } catch {
6144
+ }
6145
+ }
5633
6146
  return relation;
5634
6147
  }
5635
6148
  /**
@@ -5710,8 +6223,8 @@ var init_SQLiteStorage = __esm({
5710
6223
  this.db.exec("DELETE FROM relations");
5711
6224
  this.db.exec("DELETE FROM entities");
5712
6225
  const entityStmt = this.db.prepare(`
5713
- INSERT INTO entities (name, entityType, observations, tags, importance, parentId, createdAt, lastModified, projectId, version, parentEntityName, rootEntityName, isLatest, supersededBy)
5714
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6226
+ INSERT INTO entities (name, entityType, observations, tags, importance, parentId, createdAt, lastModified, projectId, version, parentEntityName, rootEntityName, isLatest, supersededBy, contentHash, agentMetadata)
6227
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5715
6228
  `);
5716
6229
  for (const entity of graph.entities) {
5717
6230
  entityStmt.run(
@@ -5728,7 +6241,9 @@ var init_SQLiteStorage = __esm({
5728
6241
  entity.parentEntityName ?? null,
5729
6242
  entity.rootEntityName ?? null,
5730
6243
  entity.isLatest === false ? 0 : 1,
5731
- entity.supersededBy ?? null
6244
+ entity.supersededBy ?? null,
6245
+ entity.contentHash ?? null,
6246
+ this.serializeExtensionFields(entity)
5732
6247
  );
5733
6248
  }
5734
6249
  const relationStmt = this.db.prepare(`
@@ -5776,8 +6291,8 @@ var init_SQLiteStorage = __esm({
5776
6291
  return this.mutex.runExclusive(async () => {
5777
6292
  if (!this.db) throw new Error("Database not initialized");
5778
6293
  const stmt = this.db.prepare(`
5779
- INSERT OR REPLACE INTO entities (name, entityType, observations, tags, importance, parentId, createdAt, lastModified, projectId, version, parentEntityName, rootEntityName, isLatest, supersededBy)
5780
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6294
+ INSERT OR REPLACE INTO entities (name, entityType, observations, tags, importance, parentId, createdAt, lastModified, projectId, version, parentEntityName, rootEntityName, isLatest, supersededBy, contentHash, agentMetadata)
6295
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5781
6296
  `);
5782
6297
  stmt.run(
5783
6298
  entity.name,
@@ -5793,7 +6308,9 @@ var init_SQLiteStorage = __esm({
5793
6308
  entity.parentEntityName ?? null,
5794
6309
  entity.rootEntityName ?? null,
5795
6310
  entity.isLatest === false ? 0 : 1,
5796
- entity.supersededBy ?? null
6311
+ entity.supersededBy ?? null,
6312
+ entity.contentHash ?? null,
6313
+ this.serializeExtensionFields(entity)
5797
6314
  );
5798
6315
  const existingIndex = this.cache.entities.findIndex((e) => e.name === entity.name);
5799
6316
  if (existingIndex >= 0) {
@@ -5882,7 +6399,9 @@ var init_SQLiteStorage = __esm({
5882
6399
  parentEntityName = ?,
5883
6400
  rootEntityName = ?,
5884
6401
  isLatest = ?,
5885
- supersededBy = ?
6402
+ supersededBy = ?,
6403
+ contentHash = ?,
6404
+ agentMetadata = ?
5886
6405
  WHERE name = ?
5887
6406
  `);
5888
6407
  stmt.run(
@@ -5898,6 +6417,8 @@ var init_SQLiteStorage = __esm({
5898
6417
  entity.rootEntityName ?? null,
5899
6418
  entity.isLatest === false ? 0 : 1,
5900
6419
  entity.supersededBy ?? null,
6420
+ entity.contentHash ?? null,
6421
+ this.serializeExtensionFields(entity),
5901
6422
  entityName
5902
6423
  );
5903
6424
  this.nameIndex.add(entity);
@@ -6697,6 +7218,12 @@ var init_ObservationManager = __esm({
6697
7218
  contradictionDetector;
6698
7219
  linkedEntityManager;
6699
7220
  _autoLinker;
7221
+ /** Lazy provider for the validator — invoking it constructs / fetches
7222
+ * the MemoryValidator. Stored as a thunk so unconditional wiring at
7223
+ * `ManagerContext` construction time costs nothing until the validator
7224
+ * is actually needed (i.e., MEMORY_VALIDATE_ON_STORE flips on AND an
7225
+ * observation gets added). */
7226
+ memoryValidatorProvider;
6700
7227
  /**
6701
7228
  * Enable contradiction detection on addObservations.
6702
7229
  * When a new observation is detected as contradicting an existing one,
@@ -6712,6 +7239,37 @@ var init_ObservationManager = __esm({
6712
7239
  setAutoLinker(autoLinker) {
6713
7240
  this._autoLinker = autoLinker;
6714
7241
  }
7242
+ /**
7243
+ * Wire a `MemoryValidator` provider for the optional pre-storage
7244
+ * validation hook (Phase δ.1, T31). The argument is a thunk so the
7245
+ * validator can be lazy-constructed only when actually needed —
7246
+ * `ManagerContext` wires this unconditionally at construction time so
7247
+ * runtime toggling of `MEMORY_VALIDATE_ON_STORE` works in both
7248
+ * directions, but the validator object itself isn't built until the
7249
+ * first observation is added with the flag on.
7250
+ *
7251
+ * Behaviour when flag is on:
7252
+ * - `duplicate-observation` → blocking; observation skipped with a
7253
+ * `console.warn`.
7254
+ * - `semantic-contradiction` → ADVISORY; if a `ContradictionDetector`
7255
+ * is also wired (the v1.8.0 supersede branch), that branch handles
7256
+ * the case downstream and creates a proper version chain. Filtering
7257
+ * it here would silently disable supersede semantics.
7258
+ * - `low-confidence` → ADVISORY only.
7259
+ *
7260
+ * Default off — preserves backwards-compat for existing callers.
7261
+ *
7262
+ * Overload: accepts either a validator instance (eager) or a thunk
7263
+ * (lazy). Pass the instance for tests where a stub is convenient;
7264
+ * pass the thunk for production wiring through `ManagerContext`.
7265
+ */
7266
+ setMemoryValidator(validatorOrProvider) {
7267
+ if (typeof validatorOrProvider === "function") {
7268
+ this.memoryValidatorProvider = validatorOrProvider;
7269
+ } else {
7270
+ this.memoryValidatorProvider = () => validatorOrProvider;
7271
+ }
7272
+ }
6715
7273
  /**
6716
7274
  * Resolve deduplication options from explicit parameter and environment variable.
6717
7275
  *
@@ -6780,7 +7338,31 @@ var init_ObservationManager = __esm({
6780
7338
  if (!entity) {
6781
7339
  throw new EntityNotFoundError(o.entityName);
6782
7340
  }
6783
- const nonExactDuplicates = o.contents.filter((content) => !entity.observations.includes(content));
7341
+ let nonExactDuplicates = o.contents.filter((content) => !entity.observations.includes(content));
7342
+ if (this.memoryValidatorProvider && process.env.MEMORY_VALIDATE_ON_STORE === "true") {
7343
+ const validator = this.memoryValidatorProvider();
7344
+ const passed = [];
7345
+ for (const content of nonExactDuplicates) {
7346
+ const result = await validator.validateConsistency(content, entity);
7347
+ const blockingDup = result.issues.some((i) => i.kind === "duplicate-observation");
7348
+ if (!blockingDup) {
7349
+ passed.push(content);
7350
+ if (!result.isValid) {
7351
+ const advisories = result.issues.filter((i) => i.kind !== "duplicate-observation").map((i) => i.kind);
7352
+ if (advisories.length > 0) {
7353
+ console.warn(
7354
+ `[ObservationManager] Validator advisory for "${o.entityName}": ${advisories.join(", ")}. ` + (this.contradictionDetector ? "Semantic-contradiction findings will be handled by the v1.8.0 supersede branch." : "No contradiction-detector wired; advisory only.")
7355
+ );
7356
+ }
7357
+ }
7358
+ } else {
7359
+ console.warn(
7360
+ `[ObservationManager] Skipping duplicate observation on entity "${o.entityName}". Suggestions: ${result.suggestions.join("; ")}`
7361
+ );
7362
+ }
7363
+ }
7364
+ nonExactDuplicates = passed;
7365
+ }
6784
7366
  if (nonExactDuplicates.length > 0) {
6785
7367
  if (this.contradictionDetector && this.linkedEntityManager) {
6786
7368
  const contradictions = await this.contradictionDetector.detect(
@@ -6936,6 +7518,74 @@ var init_ObservationManager = __esm({
6936
7518
  await this.storage.saveGraph(graph);
6937
7519
  }
6938
7520
  }
7521
+ // ==================== η.4.4: Temporal Validity ====================
7522
+ //
7523
+ // Per-observation temporal validity via the parallel `observationMeta[]`
7524
+ // array on Entity. Mirrors the entity-level `validFrom`/`validUntil` shape
7525
+ // but indexed by observation content (not array position) so re-ordering
7526
+ // observations doesn't disturb validity windows.
7527
+ /**
7528
+ * Mark a specific observation as no longer valid by setting its
7529
+ * `validUntil`. Creates the parallel `observationMeta[]` entry if absent
7530
+ * (preserves backwards-compat for entities that don't use the bitemporal
7531
+ * axis). Idempotent: a second call updates the existing `validUntil`.
7532
+ *
7533
+ * @throws {EntityNotFoundError} If no entity exists with the given name
7534
+ * @throws {ValidationError} If the observation isn't found on the entity
7535
+ */
7536
+ async invalidateObservation(entityName, content, ended) {
7537
+ const release = await this.storage.graphMutex.acquire();
7538
+ try {
7539
+ const graph = await this.storage.getGraphForMutation();
7540
+ const entity = graph.entities.find((e) => e.name === entityName);
7541
+ if (!entity) throw new EntityNotFoundError(entityName);
7542
+ if (!entity.observations.includes(content)) {
7543
+ throw new ValidationError(
7544
+ `Observation not found on entity '${entityName}'`,
7545
+ [`content: ${JSON.stringify(content).slice(0, 80)}`]
7546
+ );
7547
+ }
7548
+ const ts = ended ?? (/* @__PURE__ */ new Date()).toISOString();
7549
+ if (!entity.observationMeta) entity.observationMeta = [];
7550
+ const existing = entity.observationMeta.find((m) => m.content === content);
7551
+ if (existing) {
7552
+ existing.validUntil = ts;
7553
+ } else {
7554
+ entity.observationMeta.push({ content, validUntil: ts });
7555
+ }
7556
+ entity.lastModified = (/* @__PURE__ */ new Date()).toISOString();
7557
+ await this.storage.saveGraph(graph);
7558
+ } finally {
7559
+ release();
7560
+ }
7561
+ }
7562
+ /**
7563
+ * Return observations valid at a given point in time. An observation
7564
+ * with no meta entry is treated as unbounded (always valid). With a meta
7565
+ * entry, validity rules mirror `EntityManager.entityAsOf`:
7566
+ * - `validFrom` undefined OR `validFrom` <= asOf
7567
+ * - `validUntil` undefined OR `validUntil` >= asOf
7568
+ *
7569
+ * @throws {ValidationError} If `asOf` is not an ISO 8601 date string
7570
+ */
7571
+ async observationsAsOf(entityName, asOf) {
7572
+ if (!/^\d{4}-\d{2}-\d{2}/.test(asOf)) {
7573
+ throw new ValidationError(`asOf must be an ISO 8601 date string, got: '${asOf}'`, []);
7574
+ }
7575
+ const graph = await this.storage.loadGraph();
7576
+ const entity = graph.entities.find((e) => e.name === entityName);
7577
+ if (!entity) return [];
7578
+ const metaByContent = new Map(
7579
+ (entity.observationMeta ?? []).map((m) => [m.content, m])
7580
+ );
7581
+ return entity.observations.filter((obs) => {
7582
+ const meta = metaByContent.get(obs);
7583
+ if (!meta) return true;
7584
+ if (meta.validFrom && meta.validFrom > asOf) return false;
7585
+ if (meta.validUntil && meta.validUntil < asOf) return false;
7586
+ return true;
7587
+ });
7588
+ }
6939
7589
  };
6940
7590
  }
6941
7591
  });
@@ -9619,7 +10269,14 @@ var init_SavedSearchManager = __esm({
9619
10269
  try {
9620
10270
  const data = await fs4.readFile(this.savedSearchesFilePath, "utf-8");
9621
10271
  const lines = data.split("\n").filter((line) => line.trim() !== "");
9622
- return lines.map((line) => JSON.parse(line));
10272
+ const searches = [];
10273
+ for (const line of lines) {
10274
+ try {
10275
+ searches.push(JSON.parse(line));
10276
+ } catch {
10277
+ }
10278
+ }
10279
+ return searches;
9623
10280
  } catch (error) {
9624
10281
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
9625
10282
  return [];
@@ -12013,7 +12670,14 @@ var init_TagManager = __esm({
12013
12670
  try {
12014
12671
  const data = await fs5.readFile(this.tagAliasesFilePath, "utf-8");
12015
12672
  const lines = data.split("\n").filter((line) => line.trim() !== "");
12016
- return lines.map((line) => JSON.parse(line));
12673
+ const aliases = [];
12674
+ for (const line of lines) {
12675
+ try {
12676
+ aliases.push(JSON.parse(line));
12677
+ } catch {
12678
+ }
12679
+ }
12680
+ return aliases;
12017
12681
  } catch (error) {
12018
12682
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
12019
12683
  return [];
@@ -13247,6 +13911,7 @@ var init_AutoLinker = __esm({
13247
13911
  });
13248
13912
  candidates.sort((a, b) => b.name.length - a.name.length);
13249
13913
  for (const entity of candidates) {
13914
+ if (entity.name.length > 500) continue;
13250
13915
  const escapedName = escapeRegExp(entity.name);
13251
13916
  const flags = opts.caseSensitive ? "" : "i";
13252
13917
  const pattern2 = new RegExp(`\\b${escapedName}\\b`, flags);
@@ -13557,6 +14222,7 @@ var init_FactExtractor = __esm({
13557
14222
 
13558
14223
  // src/core/TransitionLedger.ts
13559
14224
  import { promises as fs7 } from "fs";
14225
+ import { randomBytes as randomBytes2 } from "crypto";
13560
14226
  import * as path4 from "path";
13561
14227
  var TransitionLedger;
13562
14228
  var init_TransitionLedger = __esm({
@@ -13591,7 +14257,7 @@ var init_TransitionLedger = __esm({
13591
14257
  async append(event) {
13592
14258
  const fullEvent = {
13593
14259
  ...event,
13594
- id: `txn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
14260
+ id: `txn_${Date.now()}_${randomBytes2(4).toString("hex")}`,
13595
14261
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
13596
14262
  };
13597
14263
  this.events.push(fullEvent);
@@ -13757,12 +14423,7 @@ var init_TransitionLedger = __esm({
13757
14423
  entityId: event.entity.name,
13758
14424
  field: "entity",
13759
14425
  from: null,
13760
- to: {
13761
- entityType: event.entity.entityType,
13762
- observations: event.entity.observations,
13763
- tags: event.entity.tags,
13764
- importance: event.entity.importance
13765
- }
14426
+ to: this.mapEntityState(event.entity)
13766
14427
  });
13767
14428
  })
13768
14429
  );
@@ -13786,12 +14447,7 @@ var init_TransitionLedger = __esm({
13786
14447
  handleAppend({
13787
14448
  entityId: event.entityName,
13788
14449
  field: "entity",
13789
- from: event.entity ? {
13790
- entityType: event.entity.entityType,
13791
- observations: event.entity.observations,
13792
- tags: event.entity.tags,
13793
- importance: event.entity.importance
13794
- } : null,
14450
+ from: event.entity ? this.mapEntityState(event.entity) : null,
13795
14451
  to: null
13796
14452
  });
13797
14453
  })
@@ -13849,6 +14505,18 @@ var init_TransitionLedger = __esm({
13849
14505
  };
13850
14506
  }
13851
14507
  // ==================== Private Helpers ====================
14508
+ /**
14509
+ * Map entity state to a consistent audit format.
14510
+ * Extracts only the core data fields to avoid metadata noise.
14511
+ */
14512
+ mapEntityState(entity) {
14513
+ return {
14514
+ entityType: entity.entityType,
14515
+ observations: entity.observations,
14516
+ tags: entity.tags,
14517
+ importance: entity.importance
14518
+ };
14519
+ }
13852
14520
  /**
13853
14521
  * Deep equality check for transition values.
13854
14522
  * Handles primitives, arrays, and plain objects.
@@ -14354,6 +15022,9 @@ var init_FreshnessManager = __esm({
14354
15022
  });
14355
15023
 
14356
15024
  // src/agent/DecayEngine.ts
15025
+ function tokenize3(text) {
15026
+ return new Set(tokenize2(text));
15027
+ }
14357
15028
  var DecayEngine;
14358
15029
  var init_DecayEngine = __esm({
14359
15030
  "src/agent/DecayEngine.ts"() {
@@ -14361,6 +15032,7 @@ var init_DecayEngine = __esm({
14361
15032
  init_esm_shims();
14362
15033
  init_agent_memory();
14363
15034
  init_FreshnessManager();
15035
+ init_textSimilarity();
14364
15036
  DecayEngine = class {
14365
15037
  storage;
14366
15038
  accessTracker;
@@ -14369,15 +15041,20 @@ var init_DecayEngine = __esm({
14369
15041
  constructor(storage, accessTracker, config = {}) {
14370
15042
  this.storage = storage;
14371
15043
  this.accessTracker = accessTracker;
15044
+ const halfLifeHours = config.halfLifeHours ?? 168;
14372
15045
  this.config = {
14373
- halfLifeHours: config.halfLifeHours ?? 168,
14374
- // 1 week
15046
+ halfLifeHours,
14375
15047
  importanceModulation: config.importanceModulation ?? true,
14376
15048
  accessModulation: config.accessModulation ?? true,
14377
15049
  minImportance: config.minImportance ?? 0.1,
14378
15050
  ttlExpiredDecayMultiplier: config.ttlExpiredDecayMultiplier ?? 3,
14379
15051
  confidenceDecayRate: config.confidenceDecayRate ?? 1e-3,
14380
- applyConfidenceToImportance: config.applyConfidenceToImportance ?? false
15052
+ applyConfidenceToImportance: config.applyConfidenceToImportance ?? false,
15053
+ // PRD MEM-01: derive decayRate from halfLifeHours when not given.
15054
+ decayRate: config.decayRate ?? Math.LN2 / (halfLifeHours * 3600),
15055
+ freshnessCoefficient: config.freshnessCoefficient ?? 0.01,
15056
+ relevanceWeight: config.relevanceWeight ?? 0.35,
15057
+ minImportanceThreshold: config.minImportanceThreshold ?? 0.1
14381
15058
  };
14382
15059
  this.freshnessManager = new FreshnessManager(storage, {
14383
15060
  defaultHalfLifeHours: this.config.halfLifeHours
@@ -14518,6 +15195,58 @@ var init_DecayEngine = __esm({
14518
15195
  getFreshnessManager() {
14519
15196
  return this.freshnessManager;
14520
15197
  }
15198
+ // ==================== PRD-aligned Effective Importance (v1.12.0) ====================
15199
+ /**
15200
+ * Calculate effective importance using the Context Engine PRD formula.
15201
+ *
15202
+ * Distinct from `calculateEffectiveImportance` (legacy formula preserved
15203
+ * for `DecayScheduler`, `SearchManager`, `SemanticForget`, etc.).
15204
+ *
15205
+ * Formula:
15206
+ * ```
15207
+ * effective = importance × recency × freshness + relevance_boost
15208
+ * recency = e^(−decayRate × age_seconds)
15209
+ * freshness = e^(−freshnessCoefficient × seconds_since_last_access)
15210
+ * relevance_boost = (|query_tokens ∩ turn_tokens| / |query_tokens|) × relevanceWeight
15211
+ * ```
15212
+ *
15213
+ * `importance` is auto-scaled from memoryjs's `[0, 10]` range to PRD's
15214
+ * `[1.0, 3.0]` range via `prd_importance = 1.0 + (memoryjs_importance / 10.0) * 2.0`.
15215
+ *
15216
+ * @param entity AgentEntity to score.
15217
+ * @param queryContext Optional query string. When provided, enables the
15218
+ * relevance-boost term via token overlap.
15219
+ * @param now Optional clock override (test injection). Defaults to `Date.now()`.
15220
+ * @returns Float in `[0, ∞)`. Callers filter via `minImportanceThreshold`.
15221
+ */
15222
+ calculatePrdEffectiveImportance(entity, queryContext, now = Date.now()) {
15223
+ const memoryjsScale = entity.importance ?? 5;
15224
+ const prdImportance = 1 + memoryjsScale / 10 * 2;
15225
+ const createdAtMs = entity.createdAt ? new Date(entity.createdAt).getTime() : now;
15226
+ const ageSeconds = Math.max(0, (now - createdAtMs) / 1e3);
15227
+ const recency = Math.exp(-this.config.decayRate * ageSeconds);
15228
+ const lastAccessMs = entity.lastAccessedAt ? new Date(entity.lastAccessedAt).getTime() : createdAtMs;
15229
+ const sinceAccessSeconds = Math.max(0, (now - lastAccessMs) / 1e3);
15230
+ const freshness = Math.exp(-this.config.freshnessCoefficient * sinceAccessSeconds);
15231
+ let relevanceBoost = 0;
15232
+ if (queryContext && queryContext.length > 0) {
15233
+ const queryTokens = tokenize3(queryContext);
15234
+ if (queryTokens.size > 0) {
15235
+ const turnText = (entity.observations ?? []).join(" ");
15236
+ const turnTokens = tokenize3(turnText);
15237
+ let intersection = 0;
15238
+ for (const t of queryTokens) {
15239
+ if (turnTokens.has(t)) intersection += 1;
15240
+ }
15241
+ relevanceBoost = intersection / queryTokens.size * this.config.relevanceWeight;
15242
+ }
15243
+ }
15244
+ return prdImportance * recency * freshness + relevanceBoost;
15245
+ }
15246
+ /** Read-only accessor for the configured PRD `min_importance_threshold`. */
15247
+ get prdMinImportanceThreshold() {
15248
+ return this.config.minImportanceThreshold;
15249
+ }
14521
15250
  // ==================== Decayed Memory Queries ====================
14522
15251
  /**
14523
15252
  * Get memories that have decayed below threshold.
@@ -15136,16 +15865,14 @@ var init_SummarizationService = __esm({
15136
15865
  * @returns Array of summaries (one per group)
15137
15866
  */
15138
15867
  async summarizeGroups(groups) {
15139
- const summaries = [];
15140
- for (const group of groups) {
15141
- if (group.length === 1) {
15142
- summaries.push(group[0]);
15143
- } else {
15144
- const summary = await this.summarize(group);
15145
- summaries.push(summary);
15146
- }
15147
- }
15148
- return summaries;
15868
+ return Promise.all(
15869
+ groups.map((group) => {
15870
+ if (group.length === 1) {
15871
+ return group[0];
15872
+ }
15873
+ return this.summarize(group);
15874
+ })
15875
+ );
15149
15876
  }
15150
15877
  // ==================== Configuration Access ====================
15151
15878
  /**
@@ -15984,7 +16711,7 @@ var init_ContextWindowManager = __esm({
15984
16711
  init_esm_shims();
15985
16712
  init_agent_memory();
15986
16713
  init_ContextProfileManager();
15987
- ContextWindowManager = class {
16714
+ ContextWindowManager = class _ContextWindowManager {
15988
16715
  storage;
15989
16716
  salienceEngine;
15990
16717
  config;
@@ -16751,39 +17478,279 @@ var init_ContextWindowManager = __esm({
16751
17478
  console.error("[ContextWindowManager.wakeUp] L1 entity loading failed:", err);
16752
17479
  }
16753
17480
  }
17481
+ if (options.compress && l1) {
17482
+ try {
17483
+ const level = typeof options.compress === "string" ? options.compress : "medium";
17484
+ const compressResult = this.compressForContext(l1, { level });
17485
+ if (compressResult.stats.savedTokens > 0) {
17486
+ l1 = compressResult.compressed;
17487
+ }
17488
+ } catch (err) {
17489
+ console.error("[ContextWindowManager.wakeUp] Compression failed, using uncompressed:", err);
17490
+ }
17491
+ }
16754
17492
  const totalTokens = this.estimateStringTokens(l0) + this.estimateStringTokens(l1);
16755
17493
  return { l0, l1, totalTokens, entityCount };
16756
17494
  }
16757
- };
16758
- }
16759
- });
16760
-
16761
- // src/agent/MemoryFormatter.ts
16762
- var MemoryFormatter;
16763
- var init_MemoryFormatter = __esm({
16764
- "src/agent/MemoryFormatter.ts"() {
16765
- "use strict";
16766
- init_esm_shims();
16767
- MemoryFormatter = class {
16768
- config;
16769
- defaultPromptTemplate;
16770
- constructor(config = {}) {
16771
- this.config = {
16772
- defaultMaxTokens: config.defaultMaxTokens ?? 2e3,
16773
- tokenMultiplier: config.tokenMultiplier ?? 1.3,
16774
- includeTimestamps: config.includeTimestamps ?? true,
16775
- includeSalience: config.includeSalience ?? false,
16776
- includeMemoryType: config.includeMemoryType ?? true,
16777
- promptTemplate: config.promptTemplate ?? ""
17495
+ // ==================== Context Compression ====================
17496
+ /** Unicode abbreviation map for aggressive-level compression (code keywords). */
17497
+ static COMMON_PATTERNS = {
17498
+ "function ": "\u0192 ",
17499
+ "return ": "\u0280 ",
17500
+ "const ": "\u1D04 ",
17501
+ "export ": "\u1D07 ",
17502
+ "import ": "\u026A ",
17503
+ "interface ": "\u026A\u0274\u1D1B ",
17504
+ "class ": "\u1D04\u029Fs ",
17505
+ "async ": "\u1D00 ",
17506
+ "await ": "\u1D00\u1D21 ",
17507
+ "undefined": "\u1D1C\u0274\u1D05",
17508
+ "null": "\u0274\u1D1C\u029F",
17509
+ "true": "\u1D1B",
17510
+ "false": "\uA730",
17511
+ "```typescript": "```ts",
17512
+ "```javascript": "```js",
17513
+ "## ": "\u2E2B ",
17514
+ "### ": "\u2E2C ",
17515
+ "#### ": "\u2E2D ",
17516
+ '"description"': '"desc"',
17517
+ '"dependencies"': '"deps"',
17518
+ '"devDependencies"': '"devDeps"',
17519
+ '"repository"': '"repo"',
17520
+ '"homepage"': '"home"',
17521
+ '"keywords"': '"keys"',
17522
+ '"license"': '"lic"',
17523
+ '"version"': '"ver"',
17524
+ '"required"': '"req"',
17525
+ '"optional"': '"opt"',
17526
+ '"default"': '"def"',
17527
+ '"example"': '"ex"',
17528
+ '"properties"': '"props"',
17529
+ '"additionalProperties"': '"addProps"',
17530
+ "node_modules/": "nm/",
17531
+ "src/": "s/",
17532
+ "dist/": "d/",
17533
+ "test/": "t/",
17534
+ "tests/": "t/",
17535
+ ".typescript": ".ts",
17536
+ ".javascript": ".js"
17537
+ };
17538
+ /**
17539
+ * Compress text for token-efficient context loading.
17540
+ * Finds repeated substrings, replaces with paragraph-sign codes, generates a legend.
17541
+ * Inspired by the CTON compress-for-context tool.
17542
+ */
17543
+ compressForContext(text, options) {
17544
+ const level = options?.level ?? "medium";
17545
+ let compressed = text;
17546
+ const legend = {};
17547
+ if (level !== "light") {
17548
+ compressed = compressed.replace(/[ \t]+/g, " ");
17549
+ compressed = compressed.replace(/\n{3,}/g, "\n\n");
17550
+ }
17551
+ if (level === "aggressive") {
17552
+ const patternResult = this.applyCommonPatterns(compressed);
17553
+ compressed = patternResult.text;
17554
+ Object.assign(legend, patternResult.legend);
17555
+ }
17556
+ const minLength2 = level === "light" ? 8 : level === "medium" ? 6 : 5;
17557
+ const minOccurrences = level === "light" ? 4 : 3;
17558
+ const maxSubstrings = level === "light" ? 20 : level === "medium" ? 30 : 36;
17559
+ const substrings = this.findRepeatedSubstrings(compressed, minLength2, minOccurrences, maxSubstrings);
17560
+ if (substrings.length > 0) {
17561
+ const totalSavings = substrings.reduce((sum, s) => sum + s.savings, 0);
17562
+ if (totalSavings > 5) {
17563
+ const result = this.applySubstringCompression(compressed, substrings);
17564
+ Object.assign(legend, result.legend);
17565
+ compressed = result.compressed;
17566
+ }
17567
+ }
17568
+ if (Object.keys(legend).length > 0) {
17569
+ const legendStr = "=== Legend ===\n" + Object.entries(legend).map(([a, f]) => `${a} = ${f}`).join("\n") + "\n=============\n\n";
17570
+ compressed = legendStr + compressed;
17571
+ }
17572
+ const originalTokens = this.estimateStringTokens(text);
17573
+ const hasLegend = Object.keys(legend).length > 0;
17574
+ const compressedTokens = hasLegend ? this.estimateStringTokens(compressed) : originalTokens;
17575
+ const savedTokens = Math.max(0, originalTokens - compressedTokens);
17576
+ return {
17577
+ compressed: hasLegend ? compressed : text,
17578
+ legend,
17579
+ stats: {
17580
+ originalTokens,
17581
+ compressedTokens,
17582
+ savedTokens,
17583
+ savedPercent: originalTokens > 0 ? Math.round(savedTokens / originalTokens * 100) : 0
17584
+ }
16778
17585
  };
16779
- this.defaultPromptTemplate = `## {name} ({type})
16780
- {observations}
16781
- {metadata}`;
16782
17586
  }
16783
- // ==================== Prompt Format ====================
16784
17587
  /**
16785
- * Format memories as text for LLM prompts.
16786
- *
17588
+ * Compress entities for context loading. Formats entities as compact text,
17589
+ * then applies compression. Sorted by importance descending.
17590
+ */
17591
+ compressEntitiesForContext(entities, options) {
17592
+ const sorted = [...entities].filter((e) => e.entityType !== "profile" && e.entityType !== "diary").sort((a, b) => (b.importance ?? 0) - (a.importance ?? 0));
17593
+ const lines = [];
17594
+ let tokenCount = 0;
17595
+ let entityCount = 0;
17596
+ const maxTokens = options?.maxTokens ?? Infinity;
17597
+ for (const e of sorted) {
17598
+ const obs = e.observations?.slice(0, 3).join("; ") ?? "";
17599
+ const line = `[${e.entityType}] ${e.name}: ${obs}`;
17600
+ const lineTokens = this.estimateStringTokens(line);
17601
+ if (tokenCount + lineTokens > maxTokens) break;
17602
+ lines.push(line);
17603
+ tokenCount += lineTokens;
17604
+ entityCount++;
17605
+ }
17606
+ const raw = lines.join("\n");
17607
+ const result = this.compressForContext(raw, { level: options?.level });
17608
+ return { ...result, entityCount };
17609
+ }
17610
+ // ==================== Compression Helpers ====================
17611
+ /**
17612
+ * Find repeated substrings and calculate compression potential.
17613
+ * Returns substrings sorted by net savings (highest first).
17614
+ * @internal
17615
+ */
17616
+ findRepeatedSubstrings(text, minLength2, minOccurrences, maxSubstrings = 36) {
17617
+ if (text.length < minLength2 * 2) return [];
17618
+ const substringCounts = /* @__PURE__ */ new Map();
17619
+ const MAX_MAP_SIZE = 1e4;
17620
+ const tokens = text.split(/(\s+|[{}()\[\]<>:;,."'`|=])/);
17621
+ outer: for (let n = 1; n <= 6; n++) {
17622
+ for (let i = 0; i <= tokens.length - n; i++) {
17623
+ const ngram = tokens.slice(i, i + n).join("");
17624
+ if (ngram.length < minLength2 || ngram.length > 50) continue;
17625
+ if (/^\s*$/.test(ngram)) continue;
17626
+ if ((ngram.match(/\s/g) || []).length > ngram.length * 0.5) continue;
17627
+ const opens = (ngram.match(/[{(\[<]/g) || []).length;
17628
+ const closes = (ngram.match(/[})\]>]/g) || []).length;
17629
+ if (opens !== closes) continue;
17630
+ substringCounts.set(ngram, (substringCounts.get(ngram) || 0) + 1);
17631
+ if (substringCounts.size >= MAX_MAP_SIZE) break outer;
17632
+ }
17633
+ }
17634
+ const pathRe = /[a-zA-Z0-9_\-./]+\/[a-zA-Z0-9_\-./]+/g;
17635
+ let pathMatch;
17636
+ while ((pathMatch = pathRe.exec(text)) !== null) {
17637
+ const p = pathMatch[0];
17638
+ if (p.length >= minLength2 && substringCounts.size < MAX_MAP_SIZE) {
17639
+ substringCounts.set(p, (substringCounts.get(p) || 0) + 1);
17640
+ }
17641
+ }
17642
+ const candidates = [];
17643
+ for (const [substring, _mapCount] of substringCounts.entries()) {
17644
+ const actualCount = text.split(substring).length - 1;
17645
+ if (actualCount >= minOccurrences) {
17646
+ const abbrevLength = 2;
17647
+ const legendCost = abbrevLength + substring.length + 4;
17648
+ const savingsPerOccurrence = substring.length - abbrevLength;
17649
+ const netSavings = savingsPerOccurrence * actualCount - legendCost;
17650
+ if (netSavings > 5) {
17651
+ candidates.push({ substring, count: actualCount, savings: netSavings });
17652
+ }
17653
+ }
17654
+ }
17655
+ candidates.sort((a, b) => b.savings - a.savings);
17656
+ const selected = [];
17657
+ const usedSubstrings = [];
17658
+ for (const candidate of candidates) {
17659
+ let isTooSimilar = false;
17660
+ const candidateTrimmed = candidate.substring.trim();
17661
+ if (candidateTrimmed.length < 3) continue;
17662
+ for (const used of usedSubstrings) {
17663
+ const usedTrimmed = used.trim();
17664
+ if (used.includes(candidate.substring) || candidate.substring.includes(used)) {
17665
+ isTooSimilar = true;
17666
+ break;
17667
+ }
17668
+ if (candidateTrimmed === usedTrimmed || candidateTrimmed.includes(usedTrimmed) || usedTrimmed.includes(candidateTrimmed)) {
17669
+ isTooSimilar = true;
17670
+ break;
17671
+ }
17672
+ const shorter = candidateTrimmed.length < usedTrimmed.length ? candidateTrimmed : usedTrimmed;
17673
+ const longer = candidateTrimmed.length >= usedTrimmed.length ? candidateTrimmed : usedTrimmed;
17674
+ if (longer.includes(shorter.slice(0, Math.floor(shorter.length * 0.7)))) {
17675
+ isTooSimilar = true;
17676
+ break;
17677
+ }
17678
+ }
17679
+ if (!isTooSimilar) {
17680
+ selected.push(candidate);
17681
+ usedSubstrings.push(candidate.substring);
17682
+ if (selected.length >= maxSubstrings) break;
17683
+ }
17684
+ }
17685
+ return selected;
17686
+ }
17687
+ /**
17688
+ * Apply substring replacements to text.
17689
+ * Applies replacements in savings-descending order (as returned by findRepeatedSubstrings).
17690
+ * @internal
17691
+ */
17692
+ applySubstringCompression(text, substrings) {
17693
+ const legend = {};
17694
+ let compressed = text;
17695
+ substrings.forEach((item, index) => {
17696
+ const abbrev = `\xA7${index.toString(36)}`;
17697
+ legend[abbrev] = item.substring;
17698
+ compressed = compressed.split(item.substring).join(abbrev);
17699
+ });
17700
+ return { compressed, legend };
17701
+ }
17702
+ /**
17703
+ * Apply COMMON_PATTERNS unicode abbreviations (aggressive level only).
17704
+ * @internal
17705
+ */
17706
+ applyCommonPatterns(text) {
17707
+ let result = text;
17708
+ const legend = {};
17709
+ for (const [pattern2, replacement] of Object.entries(_ContextWindowManager.COMMON_PATTERNS)) {
17710
+ try {
17711
+ if (!result.includes(pattern2)) continue;
17712
+ const count = result.split(pattern2).length - 1;
17713
+ const savings = (pattern2.length - replacement.length) * count;
17714
+ if (savings > pattern2.length + replacement.length + 5) {
17715
+ legend[replacement] = pattern2;
17716
+ result = result.split(pattern2).join(replacement);
17717
+ }
17718
+ } catch {
17719
+ continue;
17720
+ }
17721
+ }
17722
+ return { text: result, legend };
17723
+ }
17724
+ };
17725
+ }
17726
+ });
17727
+
17728
+ // src/agent/MemoryFormatter.ts
17729
+ var MemoryFormatter;
17730
+ var init_MemoryFormatter = __esm({
17731
+ "src/agent/MemoryFormatter.ts"() {
17732
+ "use strict";
17733
+ init_esm_shims();
17734
+ MemoryFormatter = class {
17735
+ config;
17736
+ defaultPromptTemplate;
17737
+ constructor(config = {}) {
17738
+ this.config = {
17739
+ defaultMaxTokens: config.defaultMaxTokens ?? 2e3,
17740
+ tokenMultiplier: config.tokenMultiplier ?? 1.3,
17741
+ includeTimestamps: config.includeTimestamps ?? true,
17742
+ includeSalience: config.includeSalience ?? false,
17743
+ includeMemoryType: config.includeMemoryType ?? true,
17744
+ promptTemplate: config.promptTemplate ?? ""
17745
+ };
17746
+ this.defaultPromptTemplate = `## {name} ({type})
17747
+ {observations}
17748
+ {metadata}`;
17749
+ }
17750
+ // ==================== Prompt Format ====================
17751
+ /**
17752
+ * Format memories as text for LLM prompts.
17753
+ *
16787
17754
  * @param memories - Memories to format
16788
17755
  * @param options - Formatting options
16789
17756
  * @returns Formatted text string
@@ -17711,6 +18678,7 @@ var init_WorkingMemoryManager = __esm({
17711
18678
  });
17712
18679
 
17713
18680
  // src/agent/SessionManager.ts
18681
+ import { randomBytes as randomBytes3 } from "crypto";
17714
18682
  var SessionManager;
17715
18683
  var init_SessionManager = __esm({
17716
18684
  "src/agent/SessionManager.ts"() {
@@ -17744,7 +18712,7 @@ var init_SessionManager = __esm({
17744
18712
  */
17745
18713
  generateSessionId() {
17746
18714
  const timestamp = Date.now();
17747
- const random = Math.random().toString(36).substring(2, 8);
18715
+ const random = randomBytes3(4).toString("hex");
17748
18716
  return `session_${timestamp}_${random}`;
17749
18717
  }
17750
18718
  // ==================== Session Creation ====================
@@ -18166,6 +19134,7 @@ var init_SessionManager = __esm({
18166
19134
  });
18167
19135
 
18168
19136
  // src/agent/EpisodicMemoryManager.ts
19137
+ import { randomBytes as randomBytes4 } from "crypto";
18169
19138
  var EpisodicRelations, EpisodicMemoryManager;
18170
19139
  var init_EpisodicMemoryManager = __esm({
18171
19140
  "src/agent/EpisodicMemoryManager.ts"() {
@@ -18205,7 +19174,7 @@ var init_EpisodicMemoryManager = __esm({
18205
19174
  async createEpisode(content, options) {
18206
19175
  const now = (/* @__PURE__ */ new Date()).toISOString();
18207
19176
  const timestamp = Date.now();
18208
- const name = `episode_${timestamp}_${Math.random().toString(36).slice(2, 8)}`;
19177
+ const name = `episode_${timestamp}_${randomBytes4(4).toString("hex")}`;
18209
19178
  const entity = {
18210
19179
  name,
18211
19180
  entityType: options?.entityType ?? "episode",
@@ -20044,35 +21013,41 @@ var init_VisibilityResolver = __esm({
20044
21013
  * @param requestingAgentId - ID of the agent requesting access
20045
21014
  * @param requestingMeta - Metadata for the requesting agent (undefined = unregistered)
20046
21015
  * @param ownerMeta - Metadata for the owning agent (undefined = unknown owner)
21016
+ * @param now - Override for the current time (ISO 8601). Defaults
21017
+ * to `new Date().toISOString()`. Useful for tests
21018
+ * and for evaluating access at a hypothetical time.
20047
21019
  * @returns True if access is permitted
20048
21020
  */
20049
- canAccess(memory, requestingAgentId, requestingMeta, ownerMeta) {
21021
+ canAccess(memory, requestingAgentId, requestingMeta, ownerMeta, now) {
21022
+ const currentTime = now ?? (/* @__PURE__ */ new Date()).toISOString();
21023
+ if (memory.visibleFrom && memory.visibleFrom > currentTime) return false;
21024
+ if (memory.visibleUntil && memory.visibleUntil < currentTime) return false;
20050
21025
  if (memory.agentId === requestingAgentId) {
20051
21026
  return true;
20052
21027
  }
20053
21028
  const visibility = memory.visibility ?? "private";
21029
+ let levelGrant = false;
20054
21030
  if (visibility === "public") {
20055
- return true;
20056
- }
20057
- if (!requestingMeta) {
21031
+ levelGrant = true;
21032
+ } else if (!requestingMeta) {
20058
21033
  return false;
20059
- }
20060
- if (visibility === "shared") {
20061
- return true;
20062
- }
20063
- if (visibility === "org") {
21034
+ } else if (visibility === "shared") {
21035
+ levelGrant = true;
21036
+ } else if (visibility === "org") {
20064
21037
  const requesterOrg = requestingMeta.groupMembership?.org;
20065
21038
  const ownerOrg = ownerMeta?.groupMembership?.org;
20066
- if (!requesterOrg || !ownerOrg) return false;
20067
- return requesterOrg === ownerOrg;
20068
- }
20069
- if (visibility === "team") {
21039
+ levelGrant = !!requesterOrg && !!ownerOrg && requesterOrg === ownerOrg;
21040
+ } else if (visibility === "team") {
20070
21041
  const requesterTeams = requestingMeta.groupMembership?.teams ?? [];
20071
21042
  const ownerTeams = ownerMeta?.groupMembership?.teams ?? [];
20072
- if (requesterTeams.length === 0 || ownerTeams.length === 0) return false;
20073
- return requesterTeams.some((t) => ownerTeams.includes(t));
21043
+ levelGrant = requesterTeams.length > 0 && ownerTeams.length > 0 && requesterTeams.some((t) => ownerTeams.includes(t));
20074
21044
  }
20075
- return false;
21045
+ if (!levelGrant) return false;
21046
+ if (memory.allowedRoles && memory.allowedRoles.length > 0) {
21047
+ const role = requestingMeta?.role;
21048
+ if (!role || !memory.allowedRoles.includes(role)) return false;
21049
+ }
21050
+ return true;
20076
21051
  }
20077
21052
  };
20078
21053
  }
@@ -21119,6 +22094,7 @@ var init_SessionCheckpoint = __esm({
21119
22094
  });
21120
22095
 
21121
22096
  // src/agent/WorkThreadManager.ts
22097
+ import { randomBytes as randomBytes5 } from "crypto";
21122
22098
  var VALID_TRANSITIONS, CHILD_OF_RELATION, BLOCKED_BY_RELATION, WORK_THREAD_ENTITY_TYPE, WorkThreadManager;
21123
22099
  var init_WorkThreadManager = __esm({
21124
22100
  "src/agent/WorkThreadManager.ts"() {
@@ -21192,7 +22168,7 @@ var init_WorkThreadManager = __esm({
21192
22168
  throw new Error(`Priority must be between 0 and 10, got ${options.priority}`);
21193
22169
  }
21194
22170
  const now = (/* @__PURE__ */ new Date()).toISOString();
21195
- const id = `thread_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
22171
+ const id = `thread_${Date.now()}_${randomBytes5(4).toString("hex")}`;
21196
22172
  const thread = {
21197
22173
  id,
21198
22174
  title,
@@ -22920,8 +23896,9 @@ var init_artifact = __esm({
22920
23896
  });
22921
23897
 
22922
23898
  // src/agent/ArtifactManager.ts
23899
+ import { randomBytes as randomBytes6 } from "crypto";
22923
23900
  function generateShortId() {
22924
- return Math.floor(Math.random() * 65535).toString(16).padStart(4, "0");
23901
+ return randomBytes6(2).toString("hex");
22925
23902
  }
22926
23903
  function formatDateUTC(date) {
22927
23904
  const y = date.getUTCFullYear();
@@ -23707,152 +24684,2289 @@ var init_SemanticForget = __esm({
23707
24684
  }
23708
24685
  });
23709
24686
 
23710
- // src/core/ManagerContext.ts
23711
- import path5 from "path";
23712
- var ManagerContext;
23713
- var init_ManagerContext = __esm({
23714
- "src/core/ManagerContext.ts"() {
24687
+ // src/agent/MemoryEngine.ts
24688
+ import { EventEmitter as EventEmitter6 } from "events";
24689
+ import { createHash } from "crypto";
24690
+ function stripRolePrefix(text) {
24691
+ return text.replace(ROLE_PREFIX_RE, "");
24692
+ }
24693
+ function longestCommonPrefix(a, b) {
24694
+ const max2 = Math.min(a.length, b.length);
24695
+ let i = 0;
24696
+ while (i < max2 && a[i] === b[i]) i += 1;
24697
+ return a.slice(0, i);
24698
+ }
24699
+ function tokeniseForDedup(text) {
24700
+ return new Set(
24701
+ text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((t) => t.length > 0)
24702
+ );
24703
+ }
24704
+ function hasStoreEmbedding(storage) {
24705
+ return typeof storage?.storeEmbedding === "function";
24706
+ }
24707
+ var ROLE_PREFIX_RE, MemoryEngine;
24708
+ var init_MemoryEngine = __esm({
24709
+ "src/agent/MemoryEngine.ts"() {
23715
24710
  "use strict";
23716
24711
  init_esm_shims();
23717
- init_StorageFactory();
23718
- init_EntityManager();
23719
- init_RelationManager();
23720
- init_ObservationManager();
23721
- init_HierarchyManager();
23722
- init_GraphTraversal();
23723
- init_SearchManager();
23724
- init_RankedSearch();
23725
- init_LLMQueryPlanner();
23726
- init_LLMSearchExecutor();
23727
- init_search();
23728
- init_IOManager();
23729
- init_TagManager();
23730
- init_AnalyticsManager();
23731
- init_CompressionManager();
23732
- init_ArchiveManager();
23733
- init_AutoLinker();
23734
- init_FactExtractor();
23735
- init_TransitionLedger();
23736
- init_AccessTracker();
23737
- init_DecayEngine();
23738
- init_DecayScheduler();
23739
- init_ConsolidationScheduler();
23740
- init_SalienceEngine();
23741
- init_ContextWindowManager();
23742
- init_MemoryFormatter();
23743
- init_AgentMemoryManager();
23744
- init_ArtifactManager();
23745
- init_DreamEngine();
23746
- init_RefIndex();
23747
- init_ObserverPipeline();
23748
- init_constants();
23749
- init_utils();
23750
- init_ContradictionDetector();
23751
- init_SemanticForget();
23752
- ManagerContext = class {
23753
- // Type as GraphStorage for manager compatibility; actual instance may be SQLiteStorage
23754
- // which implements the same interface via duck typing
23755
- storage;
23756
- defaultProjectId;
23757
- savedSearchesFilePath;
23758
- tagAliasesFilePath;
23759
- refIndexFilePath;
23760
- _observerPipeline;
23761
- // ==================== LAZY-INITIALIZED CORE MANAGERS ====================
23762
- _entityManager;
23763
- _relationManager;
23764
- _observationManager;
23765
- _hierarchyManager;
23766
- _graphTraversal;
23767
- _searchManager;
23768
- _rankedSearch;
23769
- _ioManager;
23770
- _tagManager;
23771
- _analyticsManager;
23772
- _compressionManager;
23773
- _archiveManager;
23774
- _autoLinker;
23775
- _factExtractor;
23776
- _transitionLedger;
23777
- _semanticSearch;
23778
- _accessTracker;
23779
- _decayEngine;
23780
- _decayScheduler;
23781
- _salienceEngine;
23782
- _contextWindowManager;
23783
- _memoryFormatter;
23784
- _agentMemory;
23785
- _refIndex;
23786
- _artifactManager;
23787
- _consolidationScheduler;
23788
- _dreamEngine;
23789
- _llmQueryPlanner;
23790
- _llmSearchExecutor;
23791
- _semanticForget;
23792
- constructor(pathOrOptions) {
23793
- const opts = typeof pathOrOptions === "string" ? { storagePath: pathOrOptions } : pathOrOptions;
23794
- this.defaultProjectId = opts.defaultProjectId;
23795
- const validatedPath = validateFilePath(opts.storagePath);
23796
- const dir = path5.dirname(validatedPath);
23797
- const basename2 = path5.basename(validatedPath, path5.extname(validatedPath));
23798
- this.savedSearchesFilePath = path5.join(dir, `${basename2}-saved-searches.jsonl`);
23799
- this.tagAliasesFilePath = path5.join(dir, `${basename2}-tag-aliases.jsonl`);
23800
- this.refIndexFilePath = path5.join(dir, `${basename2}-ref-index.jsonl`);
23801
- this.storage = createStorageFromPath(validatedPath);
23802
- if (opts.enableContradictionDetection) {
23803
- this.initContradictionDetection(opts.contradictionThreshold);
24712
+ ROLE_PREFIX_RE = /^\[role=[a-z]+\]\s*/i;
24713
+ MemoryEngine = class {
24714
+ events = new EventEmitter6();
24715
+ /** Dependencies bundle — populated in constructor, consumed in T5–T10. */
24716
+ deps;
24717
+ /** Resolved config — populated in constructor, consumed in T5–T10. */
24718
+ cfg;
24719
+ constructor(storage, entityManager, episodicMemory, workingMemory, importanceScorer, semanticSearch, embeddingService, config = {}) {
24720
+ if (config.semanticDedupEnabled && !semanticSearch) {
24721
+ throw new TypeError(
24722
+ "MemoryEngine: semanticDedupEnabled=true requires a SemanticSearch instance"
24723
+ );
23804
24724
  }
24725
+ this.deps = {
24726
+ storage,
24727
+ entityManager,
24728
+ episodicMemory,
24729
+ workingMemory,
24730
+ importanceScorer,
24731
+ semanticSearch,
24732
+ embeddingService
24733
+ };
24734
+ this.cfg = {
24735
+ jaccardThreshold: config.jaccardThreshold ?? 0.72,
24736
+ prefixOverlapThreshold: config.prefixOverlapThreshold ?? 0.5,
24737
+ dedupScanWindow: config.dedupScanWindow ?? 200,
24738
+ maxTurnsPerSession: config.maxTurnsPerSession ?? 1e3,
24739
+ semanticDedupEnabled: config.semanticDedupEnabled ?? false,
24740
+ semanticThreshold: config.semanticThreshold ?? 0.92,
24741
+ recentTurnsForImportance: config.recentTurnsForImportance ?? 10
24742
+ };
23805
24743
  }
23806
- /**
23807
- * Wire ContradictionDetector to ObservationManager if a semantic search
23808
- * embedding provider is available. Silently degrades when none is configured.
23809
- * @internal
23810
- */
23811
- initContradictionDetection(threshold) {
23812
- try {
23813
- const ss = this.semanticSearch;
23814
- if (!ss) {
23815
- console.warn(
23816
- "[ManagerContext] Contradiction detection requested but no embedding provider is configured. Set MEMORY_EMBEDDING_PROVIDER to enable it."
23817
- );
23818
- return;
24744
+ async addTurn(content, options) {
24745
+ const dup = await this.checkDuplicate(content, options.sessionId);
24746
+ if (dup.isDuplicate && dup.match) {
24747
+ this.events.emit("memoryEngine:duplicateDetected", {
24748
+ existingEntity: dup.match,
24749
+ attemptedContent: content,
24750
+ sessionId: options.sessionId,
24751
+ tier: dup.tier
24752
+ });
24753
+ return {
24754
+ entity: dup.match,
24755
+ duplicateDetected: true,
24756
+ duplicateOf: dup.match.name,
24757
+ duplicateTier: dup.tier,
24758
+ importanceScore: dup.match.importance ?? 0
24759
+ };
24760
+ }
24761
+ let importance;
24762
+ if (typeof options.importance === "number") {
24763
+ importance = options.importance;
24764
+ } else {
24765
+ const recentTurns = options.recentTurns ?? await this.loadRecentTurnsForImportance(options.sessionId);
24766
+ importance = this.deps.importanceScorer.score(content, {
24767
+ queryContext: options.queryContext,
24768
+ recentTurns
24769
+ });
24770
+ }
24771
+ const observation = `[role=${options.role}] ${content}`;
24772
+ const entity = await this.deps.episodicMemory.createEpisode(observation, {
24773
+ sessionId: options.sessionId,
24774
+ agentId: options.agentId,
24775
+ importance
24776
+ });
24777
+ const hash = this.computeContentHash(content);
24778
+ await this.deps.storage.updateEntity(entity.name, { contentHash: hash });
24779
+ const enriched = { ...entity, contentHash: hash };
24780
+ if (this.deps.embeddingService && hasStoreEmbedding(this.deps.storage)) {
24781
+ try {
24782
+ const vector = await this.deps.embeddingService.embed(content);
24783
+ const model = this.deps.embeddingService.getModelName?.() ?? "unknown";
24784
+ this.deps.storage.storeEmbedding(entity.name, vector, model);
24785
+ } catch {
23819
24786
  }
23820
- const detector = new ContradictionDetector(ss, threshold ?? 0.85);
23821
- this.observationManager.setContradictionDetector(detector, this.entityManager);
23822
- } catch (err) {
23823
- console.warn(
23824
- "[ManagerContext] Could not initialise contradiction detection:",
23825
- err instanceof Error ? err.message : String(err)
23826
- );
23827
24787
  }
24788
+ this.events.emit("memoryEngine:turnAdded", {
24789
+ entity: enriched,
24790
+ sessionId: options.sessionId,
24791
+ role: options.role,
24792
+ importance
24793
+ });
24794
+ return { entity: enriched, duplicateDetected: false, importanceScore: importance };
23828
24795
  }
23829
- // ==================== LAZY ACCESSORS (agent memory + semantic) ====================
23830
- /** EntityManager - Entity CRUD and tag operations */
23831
- get entityManager() {
23832
- return this._entityManager ??= new EntityManager(
23833
- this.storage,
23834
- { defaultProjectId: this.defaultProjectId }
24796
+ async loadRecentTurnsForImportance(sessionId) {
24797
+ const recent = await this.getRecentSessionEntities(
24798
+ sessionId,
24799
+ this.cfg.recentTurnsForImportance
23835
24800
  );
24801
+ return recent.map((e) => stripRolePrefix(e.observations[0] ?? ""));
23836
24802
  }
23837
- /** RelationManager - Relation CRUD */
23838
- get relationManager() {
23839
- return this._relationManager ??= new RelationManager(this.storage);
23840
- }
23841
- /** ObservationManager - Observation CRUD */
23842
- get observationManager() {
23843
- return this._observationManager ??= new ObservationManager(this.storage);
24803
+ async getSessionTurns(sessionId, options = {}) {
24804
+ const graph = await this.deps.storage.loadGraph();
24805
+ let turns = graph.entities.filter(
24806
+ (e) => e.sessionId === sessionId
24807
+ );
24808
+ turns.sort((a, b) => {
24809
+ const aT = a.createdAt ? new Date(a.createdAt).getTime() : 0;
24810
+ const bT = b.createdAt ? new Date(b.createdAt).getTime() : 0;
24811
+ return aT - bT;
24812
+ });
24813
+ if (options.role) {
24814
+ const prefix = `[role=${options.role}]`;
24815
+ turns = turns.filter((e) => (e.observations[0] ?? "").startsWith(prefix));
24816
+ }
24817
+ if (typeof options.limit === "number") {
24818
+ turns = turns.slice(0, options.limit);
24819
+ }
24820
+ return turns;
24821
+ }
24822
+ async checkDuplicate(content, sessionId) {
24823
+ if (this.cfg.semanticDedupEnabled && this.deps.semanticSearch) {
24824
+ const ts = await this.checkTierSemantic(content, sessionId);
24825
+ if (ts.isDuplicate) return ts;
24826
+ }
24827
+ const t1 = await this.checkTierExact(content, sessionId);
24828
+ if (t1.isDuplicate) return t1;
24829
+ const recent = await this.getRecentSessionEntities(sessionId, this.cfg.dedupScanWindow);
24830
+ const t2 = this.checkTierPrefix(content, recent);
24831
+ if (t2.isDuplicate) return t2;
24832
+ const t3 = this.checkTierJaccard(content, recent);
24833
+ if (t3.isDuplicate) return t3;
24834
+ return { isDuplicate: false };
24835
+ }
24836
+ async checkTierSemantic(content, sessionId) {
24837
+ if (!this.deps.semanticSearch) return { isDuplicate: false };
24838
+ const graph = await this.deps.storage.loadGraph();
24839
+ const results = await this.deps.semanticSearch.search(graph, content, 5, this.cfg.semanticThreshold);
24840
+ for (const hit of results) {
24841
+ if (hit.similarity < this.cfg.semanticThreshold) continue;
24842
+ const candidate = hit.entity;
24843
+ if (candidate.sessionId === sessionId) {
24844
+ return { isDuplicate: true, match: candidate, tier: "semantic" };
24845
+ }
24846
+ }
24847
+ return { isDuplicate: false };
24848
+ }
24849
+ computeContentHash(content) {
24850
+ return createHash("sha256").update(content).digest("hex");
24851
+ }
24852
+ async checkTierExact(content, sessionId) {
24853
+ const hash = this.computeContentHash(content);
24854
+ const graph = await this.deps.storage.loadGraph();
24855
+ const candidates = graph.entities.filter(
24856
+ (e) => e.contentHash === hash
24857
+ );
24858
+ const match = candidates.find((e) => e.sessionId === sessionId);
24859
+ if (match) return { isDuplicate: true, match, tier: "exact" };
24860
+ return { isDuplicate: false };
24861
+ }
24862
+ async getRecentSessionEntities(sessionId, windowSize) {
24863
+ const graph = await this.deps.storage.loadGraph();
24864
+ const sessionEntities = graph.entities.filter(
24865
+ (e) => e.sessionId === sessionId
24866
+ );
24867
+ sessionEntities.sort((a, b) => {
24868
+ const aT = a.createdAt ? new Date(a.createdAt).getTime() : 0;
24869
+ const bT = b.createdAt ? new Date(b.createdAt).getTime() : 0;
24870
+ return bT - aT;
24871
+ });
24872
+ return sessionEntities.slice(0, windowSize);
24873
+ }
24874
+ checkTierPrefix(content, candidates) {
24875
+ for (const candidate of candidates) {
24876
+ const candidateContent = stripRolePrefix(candidate.observations[0] ?? "");
24877
+ const shared = longestCommonPrefix(content, candidateContent);
24878
+ const ratio = shared.length / Math.max(content.length, candidateContent.length);
24879
+ if (ratio >= this.cfg.prefixOverlapThreshold) {
24880
+ return { isDuplicate: true, match: candidate, tier: "prefix" };
24881
+ }
24882
+ }
24883
+ return { isDuplicate: false };
24884
+ }
24885
+ checkTierJaccard(content, candidates) {
24886
+ const contentTokens = tokeniseForDedup(content);
24887
+ if (contentTokens.size === 0) return { isDuplicate: false };
24888
+ for (const candidate of candidates) {
24889
+ const candidateContent = stripRolePrefix(candidate.observations[0] ?? "");
24890
+ const candidateTokens = tokeniseForDedup(candidateContent);
24891
+ if (candidateTokens.size === 0) continue;
24892
+ let intersection = 0;
24893
+ for (const token of contentTokens) {
24894
+ if (candidateTokens.has(token)) intersection += 1;
24895
+ }
24896
+ const union = contentTokens.size + candidateTokens.size - intersection;
24897
+ const jaccard3 = union === 0 ? 0 : intersection / union;
24898
+ if (jaccard3 >= this.cfg.jaccardThreshold) {
24899
+ return { isDuplicate: true, match: candidate, tier: "jaccard" };
24900
+ }
24901
+ }
24902
+ return { isDuplicate: false };
24903
+ }
24904
+ async deleteSession(sessionId) {
24905
+ const turns = await this.getSessionTurns(sessionId);
24906
+ if (turns.length === 0) return { deleted: 0 };
24907
+ const names = turns.map((t) => t.name);
24908
+ await this.deps.entityManager.deleteEntities(names);
24909
+ this.events.emit("memoryEngine:sessionDeleted", {
24910
+ sessionId,
24911
+ deletedCount: names.length
24912
+ });
24913
+ return { deleted: names.length };
23844
24914
  }
23845
- /** HierarchyManager - Entity hierarchy operations */
23846
- get hierarchyManager() {
23847
- return this._hierarchyManager ??= new HierarchyManager(this.storage);
24915
+ async listSessions() {
24916
+ const graph = await this.deps.storage.loadGraph();
24917
+ const sessions = /* @__PURE__ */ new Set();
24918
+ for (const e of graph.entities) {
24919
+ const s = e.sessionId;
24920
+ if (s) sessions.add(s);
24921
+ }
24922
+ return Array.from(sessions);
23848
24923
  }
23849
- /** GraphTraversal - Phase 4 Sprint 6-8: Graph traversal algorithms */
23850
- get graphTraversal() {
23851
- return this._graphTraversal ??= new GraphTraversal(this.storage);
24924
+ };
24925
+ }
24926
+ });
24927
+
24928
+ // src/agent/ImportanceScorer.ts
24929
+ function tokenise(text) {
24930
+ return new Set(
24931
+ text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((t) => t.length > 0)
24932
+ );
24933
+ }
24934
+ function countIntersection(a, b) {
24935
+ let count = 0;
24936
+ for (const token of a) {
24937
+ if (b.has(token)) count += 1;
24938
+ }
24939
+ return count;
24940
+ }
24941
+ var ImportanceScorer;
24942
+ var init_ImportanceScorer = __esm({
24943
+ "src/agent/ImportanceScorer.ts"() {
24944
+ "use strict";
24945
+ init_esm_shims();
24946
+ ImportanceScorer = class {
24947
+ domainKeywords;
24948
+ lengthWeight;
24949
+ keywordWeight;
24950
+ overlapWeight;
24951
+ constructor(config = {}) {
24952
+ this.domainKeywords = config.domainKeywords ?? /* @__PURE__ */ new Set();
24953
+ this.lengthWeight = config.lengthWeight ?? 0.3;
24954
+ this.keywordWeight = config.keywordWeight ?? 0.4;
24955
+ this.overlapWeight = config.overlapWeight ?? 0.3;
24956
+ }
24957
+ /**
24958
+ * Score new content at creation time.
24959
+ *
24960
+ * PRD MEM-02: "Auto-importance scoring evaluates: content length
24961
+ * (log-scaled), domain keyword presence, query token overlap with
24962
+ * recent turns" (PRD §8 line 409).
24963
+ *
24964
+ * Returns integer in [0, 10] (memoryjs scale). PRD's narrower [1.0, 3.0]
24965
+ * range is out of scope here; the Decay Extensions spec owns the mapping.
24966
+ */
24967
+ score(content, options = {}) {
24968
+ if (content.length === 0) return 0;
24969
+ const contentTokens = tokenise(content);
24970
+ const lengthSignal = Math.min(1, Math.log10(content.length) / 4);
24971
+ const keywordSignal = this.domainKeywords.size > 0 ? countIntersection(contentTokens, this.domainKeywords) / this.domainKeywords.size : 0;
24972
+ const overlapCorpus = [];
24973
+ if (options.queryContext) overlapCorpus.push(options.queryContext);
24974
+ if (options.recentTurns) overlapCorpus.push(...options.recentTurns);
24975
+ let overlapSignal;
24976
+ if (overlapCorpus.length === 0) {
24977
+ overlapSignal = 0.5;
24978
+ } else {
24979
+ const corpusTokens = tokenise(overlapCorpus.join(" "));
24980
+ overlapSignal = contentTokens.size > 0 ? countIntersection(contentTokens, corpusTokens) / contentTokens.size : 0;
24981
+ }
24982
+ const raw = this.lengthWeight * lengthSignal + this.keywordWeight * keywordSignal + this.overlapWeight * overlapSignal;
24983
+ return Math.max(0, Math.min(10, Math.round(raw * 10)));
23852
24984
  }
23853
- /** SearchManager - All search operations */
23854
- get searchManager() {
23855
- return this._searchManager ??= new SearchManager(this.storage, this.savedSearchesFilePath);
24985
+ };
24986
+ }
24987
+ });
24988
+
24989
+ // src/agent/InMemoryBackend.ts
24990
+ function turnToEntity(turn) {
24991
+ const prd = turn.importance;
24992
+ const memoryjsImportance = Math.max(0, Math.min(10, (prd - 1) * 5));
24993
+ return {
24994
+ name: turn.id,
24995
+ entityType: "memory_turn",
24996
+ observations: [turn.content],
24997
+ createdAt: turn.createdAt,
24998
+ lastModified: turn.createdAt,
24999
+ lastAccessedAt: turn.lastAccessedAt,
25000
+ importance: memoryjsImportance,
25001
+ memoryType: "episodic",
25002
+ accessCount: turn.accessCount ?? 0,
25003
+ confidence: 1,
25004
+ confirmationCount: 0,
25005
+ visibility: "private",
25006
+ sessionId: turn.sessionId
25007
+ };
25008
+ }
25009
+ var InMemoryBackend;
25010
+ var init_InMemoryBackend = __esm({
25011
+ "src/agent/InMemoryBackend.ts"() {
25012
+ "use strict";
25013
+ init_esm_shims();
25014
+ InMemoryBackend = class {
25015
+ constructor(decayEngine) {
25016
+ this.decayEngine = decayEngine;
25017
+ }
25018
+ /** Per-session FIFO ordered list of turns. */
25019
+ turns = /* @__PURE__ */ new Map();
25020
+ async add(turn) {
25021
+ const list = this.turns.get(turn.sessionId) ?? [];
25022
+ if (list.some((existing) => existing.content === turn.content)) {
25023
+ return;
25024
+ }
25025
+ list.push(turn);
25026
+ this.turns.set(turn.sessionId, list);
25027
+ }
25028
+ async get_weighted(query, sessionId, options) {
25029
+ const list = this.turns.get(sessionId);
25030
+ if (!list || list.length === 0) return [];
25031
+ const threshold = options?.threshold ?? this.decayEngine.prdMinImportanceThreshold;
25032
+ const limit = options?.limit ?? 10;
25033
+ const scored = list.map((turn) => {
25034
+ const synthetic = turnToEntity(turn);
25035
+ const score = this.decayEngine.calculatePrdEffectiveImportance(
25036
+ synthetic,
25037
+ query
25038
+ );
25039
+ return { turn, score };
25040
+ });
25041
+ return scored.filter((wt) => wt.score >= threshold).sort((a, b) => b.score - a.score).slice(0, limit);
25042
+ }
25043
+ async delete_session(sessionId) {
25044
+ this.turns.delete(sessionId);
25045
+ }
25046
+ async list_sessions() {
25047
+ return Array.from(this.turns.keys());
25048
+ }
25049
+ };
25050
+ }
25051
+ });
25052
+
25053
+ // src/agent/SQLiteBackend.ts
25054
+ function entityToTurn(entity) {
25055
+ const observation = entity.observations?.[0] ?? "";
25056
+ const m = observation.match(/^\[role=([a-z]+)\]\s*(.*)$/i);
25057
+ const role = m?.[1]?.toLowerCase() ?? "user";
25058
+ const content = m?.[2] ?? observation;
25059
+ const memoryjsImportance = entity.importance ?? 5;
25060
+ const prdImportance = 1 + memoryjsImportance / 10 * 2;
25061
+ return {
25062
+ id: entity.name,
25063
+ sessionId: entity.sessionId ?? "",
25064
+ content,
25065
+ role,
25066
+ importance: prdImportance,
25067
+ createdAt: entity.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
25068
+ lastAccessedAt: entity.lastAccessedAt,
25069
+ accessCount: entity.accessCount,
25070
+ embedding: void 0,
25071
+ // not exposed via getSessionTurns; future read-path enhancement
25072
+ metadata: void 0
25073
+ // see the metadata-round-trip caveat in the contract test
25074
+ };
25075
+ }
25076
+ var SQLiteBackend;
25077
+ var init_SQLiteBackend = __esm({
25078
+ "src/agent/SQLiteBackend.ts"() {
25079
+ "use strict";
25080
+ init_esm_shims();
25081
+ SQLiteBackend = class {
25082
+ constructor(memoryEngine, decayEngine, options = {}) {
25083
+ this.memoryEngine = memoryEngine;
25084
+ this.decayEngine = decayEngine;
25085
+ this.options = {
25086
+ dedupOnAdd: options.dedupOnAdd ?? true,
25087
+ preserveCallerIds: options.preserveCallerIds ?? false
25088
+ };
25089
+ }
25090
+ options;
25091
+ async add(turn) {
25092
+ if (!this.options.dedupOnAdd) {
25093
+ throw new Error(
25094
+ "SQLiteBackend: dedupOnAdd=false bypass path is not implemented yet. See docs/superpowers/specs/2026-04-16-memory-engine-decay-extensions-design.md"
25095
+ );
25096
+ }
25097
+ const memoryjsImportance = Math.max(0, Math.min(10, (turn.importance - 1) * 5));
25098
+ const result = await this.memoryEngine.addTurn(turn.content, {
25099
+ sessionId: turn.sessionId,
25100
+ role: turn.role,
25101
+ importance: memoryjsImportance
25102
+ });
25103
+ if (this.options.preserveCallerIds && !result.duplicateDetected && turn.id !== result.entity.name) {
25104
+ throw new Error(
25105
+ "SQLiteBackend: preserveCallerIds=true requires storage.renameEntity which is not implemented yet. Default false; see \u03B2.3 spec notes."
25106
+ );
25107
+ }
25108
+ }
25109
+ async get_weighted(query, sessionId, options) {
25110
+ const turns = await this.memoryEngine.getSessionTurns(sessionId);
25111
+ if (turns.length === 0) return [];
25112
+ const threshold = options?.threshold ?? this.decayEngine.prdMinImportanceThreshold;
25113
+ const limit = options?.limit ?? 10;
25114
+ const scored = turns.map((entity) => {
25115
+ const score = this.decayEngine.calculatePrdEffectiveImportance(entity, query);
25116
+ return { turn: entityToTurn(entity), score };
25117
+ });
25118
+ return scored.filter((wt) => wt.score >= threshold).sort((a, b) => b.score - a.score).slice(0, limit);
25119
+ }
25120
+ async delete_session(sessionId) {
25121
+ await this.memoryEngine.deleteSession(sessionId);
25122
+ }
25123
+ async list_sessions() {
25124
+ return await this.memoryEngine.listSessions();
25125
+ }
25126
+ };
25127
+ }
25128
+ });
25129
+
25130
+ // src/agent/MemoryValidator.ts
25131
+ function rawToTyped(c) {
25132
+ const sev = c.similarity >= 0.95 ? "high" : c.similarity >= 0.85 ? "medium" : "low";
25133
+ return {
25134
+ observation1: c.existingObservation,
25135
+ observation2: c.newObservation,
25136
+ conflictType: "factual",
25137
+ severity: sev
25138
+ };
25139
+ }
25140
+ var MemoryValidator;
25141
+ var init_MemoryValidator = __esm({
25142
+ "src/agent/MemoryValidator.ts"() {
25143
+ "use strict";
25144
+ init_esm_shims();
25145
+ MemoryValidator = class {
25146
+ contradictionDetector;
25147
+ lowConfidenceThreshold;
25148
+ constructor(contradictionDetector, config = {}) {
25149
+ this.contradictionDetector = contradictionDetector;
25150
+ this.lowConfidenceThreshold = config.lowConfidenceThreshold ?? 0.4;
25151
+ }
25152
+ /**
25153
+ * Check a new observation against an entity's existing knowledge.
25154
+ * Composite: runs the contradiction detector, plus duplicate detection,
25155
+ * plus low-confidence flag if the entity itself looks unreliable.
25156
+ */
25157
+ async validateConsistency(newObservation, existing) {
25158
+ const issues = [];
25159
+ const suggestions = [];
25160
+ if (existing.observations.includes(newObservation)) {
25161
+ issues.push({
25162
+ kind: "duplicate-observation",
25163
+ message: "Identical observation already present on this entity.",
25164
+ observation: newObservation
25165
+ });
25166
+ suggestions.push("Drop the duplicate; existing observation is unchanged.");
25167
+ }
25168
+ const contradictions = await this.contradictionDetector.detect(existing, [newObservation]);
25169
+ for (const c of contradictions) {
25170
+ issues.push({
25171
+ kind: "semantic-contradiction",
25172
+ message: `New observation semantically contradicts existing observation (similarity ${c.similarity.toFixed(2)}).`,
25173
+ observation: newObservation
25174
+ });
25175
+ suggestions.push(
25176
+ "Either supersede the existing observation (use ContradictionDetector.supersede) or reject the new one."
25177
+ );
25178
+ }
25179
+ const reliability = this.calculateReliability(existing);
25180
+ if (reliability < this.lowConfidenceThreshold) {
25181
+ issues.push({
25182
+ kind: "low-confidence",
25183
+ message: `Entity reliability ${reliability.toFixed(2)} is below threshold ${this.lowConfidenceThreshold}.`
25184
+ });
25185
+ }
25186
+ return {
25187
+ isValid: issues.filter((i) => i.kind !== "low-confidence").length === 0,
25188
+ confidence: 1 - 0.2 * issues.length,
25189
+ // rough confidence drop per issue
25190
+ issues,
25191
+ suggestions
25192
+ };
25193
+ }
25194
+ /**
25195
+ * Detect contradictions within an entity's own observation set.
25196
+ * Delegates to `ContradictionDetector.detect` against a synthetic
25197
+ * "new observations" set (every observation paired against the rest).
25198
+ *
25199
+ * Per ROADMAP §3B.1 spec, returns the extended Contradiction shape
25200
+ * (with `conflictType` + `severity`) — these are derived heuristically
25201
+ * from similarity score because the underlying detector is similarity-
25202
+ * only.
25203
+ */
25204
+ async detectContradictions(entity) {
25205
+ if (entity.observations.length < 2) return [];
25206
+ const raw = await this.contradictionDetector.detect(entity, entity.observations);
25207
+ const out = [];
25208
+ const seen = /* @__PURE__ */ new Set();
25209
+ for (const c of raw) {
25210
+ const key = [c.existingObservation, c.newObservation].sort().join("||");
25211
+ if (seen.has(key)) continue;
25212
+ seen.add(key);
25213
+ out.push(rawToTyped(c));
25214
+ }
25215
+ return out;
25216
+ }
25217
+ /**
25218
+ * Apply feedback to repair an entity by appending a corrective
25219
+ * observation prefixed with `[repair]`. Returns the repaired entity
25220
+ * — does NOT persist. Caller decides via
25221
+ * `EntityManager.updateEntity` or supersede semantics.
25222
+ *
25223
+ * For full `ConflictResolver`-driven repair against a competing
25224
+ * memory, see `repairWithResolver`.
25225
+ */
25226
+ async repairMemory(entity, feedback) {
25227
+ return {
25228
+ ...entity,
25229
+ observations: [...entity.observations, `[repair] ${feedback}`],
25230
+ lastModified: (/* @__PURE__ */ new Date()).toISOString()
25231
+ };
25232
+ }
25233
+ /**
25234
+ * Repair an entity by delegating to a `ConflictResolver`. Constructs
25235
+ * the minimal `ConflictInfo` from a `Contradiction` finding so callers
25236
+ * don't have to hand-build it. Closes the loop spec'd in ROADMAP §3B.1
25237
+ * for `repairMemory` integration.
25238
+ *
25239
+ * @param entity Primary memory being repaired (must be `AgentEntity`).
25240
+ * @param competing Competing memory to resolve against.
25241
+ * @param contradiction Optional similarity score / context. Severity
25242
+ * is mapped onto `detectionMethod = 'similarity'`.
25243
+ * @param resolver The `ConflictResolver` instance.
25244
+ * @param agents Optional agent-metadata registry (used by the
25245
+ * `trusted_agent` strategy). Empty Map is fine
25246
+ * for the strategies that don't need it.
25247
+ * @returns The resolved memory per the resolver's verdict.
25248
+ *
25249
+ * Throws when neither input is an `AgentEntity` (resolver requires the
25250
+ * extension fields), or when the resolver itself throws (e.g., no
25251
+ * conflicting memories found — should not happen given we provide both).
25252
+ */
25253
+ async repairWithResolver(entity, competing, resolver, options = {}) {
25254
+ const sim = options.contradiction?.similarity ?? 0.85;
25255
+ const detectionMethod = options.detectionMethod ?? "similarity";
25256
+ const agents = options.agents ?? /* @__PURE__ */ new Map();
25257
+ let suggestedStrategy;
25258
+ if (options.strategy) {
25259
+ suggestedStrategy = options.strategy;
25260
+ } else {
25261
+ const aTs = entity.lastModified ? Date.parse(entity.lastModified) : 0;
25262
+ const bTs = competing.lastModified ? Date.parse(competing.lastModified) : 0;
25263
+ const ageDeltaSeconds = Math.abs(aTs - bTs) / 1e3;
25264
+ suggestedStrategy = ageDeltaSeconds > 60 * 60 * 24 ? "most_recent" : "highest_confidence";
25265
+ }
25266
+ const result = resolver.resolveConflict(
25267
+ {
25268
+ primaryMemory: entity.name,
25269
+ conflictingMemories: [competing.name],
25270
+ detectionMethod,
25271
+ similarityScore: sim,
25272
+ suggestedStrategy,
25273
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString()
25274
+ },
25275
+ [entity, competing],
25276
+ agents
25277
+ );
25278
+ return result.resolvedMemory;
25279
+ }
25280
+ /**
25281
+ * Validate temporal consistency of observations carrying ISO-8601
25282
+ * timestamps. Looks for either explicit `[T=ISO]` prefixes or
25283
+ * `createdAt`-style metadata. Returns `isValid: false` when any
25284
+ * adjacent pair is out of order.
25285
+ *
25286
+ * Synchronous (no I/O); the spec method is sync.
25287
+ */
25288
+ validateTemporalOrder(observations) {
25289
+ const issues = [];
25290
+ const stamped = [];
25291
+ for (let i = 0; i < observations.length; i += 1) {
25292
+ const m = observations[i].match(/\[T=([^\]]+)\]/);
25293
+ if (m) {
25294
+ const ts = Date.parse(m[1]);
25295
+ if (Number.isFinite(ts)) {
25296
+ stamped.push({ idx: i, ts, obs: observations[i] });
25297
+ }
25298
+ }
25299
+ }
25300
+ for (let i = 1; i < stamped.length; i += 1) {
25301
+ if (stamped[i].ts < stamped[i - 1].ts) {
25302
+ issues.push({
25303
+ kind: "temporal-disorder",
25304
+ message: `Observation at index ${stamped[i].idx} has earlier timestamp than index ${stamped[i - 1].idx}.`,
25305
+ observation: stamped[i].obs
25306
+ });
25307
+ }
25308
+ }
25309
+ return {
25310
+ isValid: issues.length === 0,
25311
+ confidence: stamped.length >= 2 ? 0.9 : 0.5,
25312
+ // less confident with sparse stamps
25313
+ issues,
25314
+ suggestions: issues.length > 0 ? ["Re-sort observations by parsed timestamp."] : []
25315
+ };
25316
+ }
25317
+ /**
25318
+ * Reliability score in `[0, 1]`. Composite of:
25319
+ * - the entity's own `confidence` field (default 0.5 when absent),
25320
+ * - confirmation count (asymptotic — diminishing returns past 5),
25321
+ * - inverse decay from creation time (older = slightly less reliable
25322
+ * absent reinforcement; very gentle, doesn't dominate).
25323
+ *
25324
+ * Read-only; does not persist anything to the entity.
25325
+ */
25326
+ calculateReliability(entity) {
25327
+ const ag = entity;
25328
+ const conf = ag.confidence ?? 0.5;
25329
+ const confirmations = ag.confirmationCount ?? 0;
25330
+ const confFactor = confirmations === 0 ? 0 : 1 - 1 / (1 + confirmations / 5);
25331
+ const created = entity.createdAt ? Date.parse(entity.createdAt) : Date.now();
25332
+ const ageDays = Math.max(0, (Date.now() - created) / (1e3 * 60 * 60 * 24));
25333
+ const agePenalty = Math.min(0.3, ageDays / 300);
25334
+ return Math.max(0, Math.min(1, conf * 0.6 + confFactor * 0.3 - agePenalty * 0.1));
25335
+ }
25336
+ };
25337
+ }
25338
+ });
25339
+
25340
+ // src/agent/TrajectoryCompressor.ts
25341
+ function tokenize4(text) {
25342
+ return new Set(
25343
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((t) => t.length > 0)
25344
+ );
25345
+ }
25346
+ function jaccard(a, b) {
25347
+ if (a.size === 0 && b.size === 0) return 0;
25348
+ let inter = 0;
25349
+ for (const t of a) if (b.has(t)) inter += 1;
25350
+ return inter / (a.size + b.size - inter);
25351
+ }
25352
+ function pickTop3(observations) {
25353
+ if (observations.length <= 3) return observations;
25354
+ const tokens = observations.map(tokenize4);
25355
+ const scores = tokens.map((set, i) => {
25356
+ let overlap = 0;
25357
+ for (let j = 0; j < tokens.length; j += 1) {
25358
+ if (i !== j) {
25359
+ for (const t of set) if (tokens[j].has(t)) overlap += 1;
25360
+ }
25361
+ }
25362
+ return { idx: i, score: overlap };
25363
+ });
25364
+ scores.sort((a, b) => b.score - a.score);
25365
+ return scores.slice(0, 3).map((s) => observations[s.idx]);
25366
+ }
25367
+ var TrajectoryCompressor;
25368
+ var init_TrajectoryCompressor = __esm({
25369
+ "src/agent/TrajectoryCompressor.ts"() {
25370
+ "use strict";
25371
+ init_esm_shims();
25372
+ TrajectoryCompressor = class {
25373
+ contextWindow;
25374
+ redundancyThreshold;
25375
+ constructor(contextWindow, config = {}) {
25376
+ this.contextWindow = contextWindow;
25377
+ this.redundancyThreshold = config.redundancyThreshold ?? 0.7;
25378
+ }
25379
+ /**
25380
+ * Compress an observation sequence into a CompressedMemory.
25381
+ * Strategy: keep observations whose tokens overlap heavily with the
25382
+ * majority (these are the "core" facts), drop low-overlap outliers.
25383
+ * Length-truncate the summary at `maxLength`. No LLM dependency yet —
25384
+ * pluggable later via a summarizer config option.
25385
+ */
25386
+ async distill(observations, options = {}) {
25387
+ const preserveTemporalOrder = options.preserveTemporalOrder ?? true;
25388
+ const maxLength2 = options.maxLength ?? 2e3;
25389
+ void options.preserveEntities;
25390
+ const originalCount = observations.length;
25391
+ if (originalCount === 0) {
25392
+ return {
25393
+ summary: "",
25394
+ keyFacts: [],
25395
+ originalCount: 0,
25396
+ compressionRatio: 0,
25397
+ preservedDetails: [],
25398
+ discardedDetails: []
25399
+ };
25400
+ }
25401
+ const tokenSets = observations.map((o) => tokenize4(o));
25402
+ const scores = tokenSets.map((set, i) => {
25403
+ let overlap = 0;
25404
+ for (let j = 0; j < tokenSets.length; j += 1) {
25405
+ if (i === j) continue;
25406
+ for (const t of set) if (tokenSets[j].has(t)) overlap += 1;
25407
+ }
25408
+ return { idx: i, score: overlap, obs: observations[i] };
25409
+ });
25410
+ const threshold = options.importanceThreshold ?? 0;
25411
+ const kept = scores.filter((s) => s.score >= threshold);
25412
+ const dropped = scores.filter((s) => s.score < threshold);
25413
+ if (preserveTemporalOrder) kept.sort((a, b) => a.idx - b.idx);
25414
+ else kept.sort((a, b) => b.score - a.score);
25415
+ const keyFacts = kept.slice(0, Math.min(kept.length, 10)).map((s) => s.obs);
25416
+ let summary = keyFacts.join(" ");
25417
+ if (summary.length > maxLength2) summary = summary.slice(0, maxLength2).trimEnd() + "\u2026";
25418
+ const totalLength = observations.join(" ").length || 1;
25419
+ return {
25420
+ summary,
25421
+ keyFacts,
25422
+ originalCount,
25423
+ compressionRatio: summary.length / totalLength,
25424
+ preservedDetails: kept.map((s) => s.obs),
25425
+ discardedDetails: dropped.map((s) => s.obs)
25426
+ };
25427
+ }
25428
+ /**
25429
+ * Produce a coarsened view of a set of entities at one of three
25430
+ * granularities. `fine` returns the entities unchanged; `medium`
25431
+ * trims observations to the top-3 most overlap-y per entity;
25432
+ * `coarse` distills each entity's observations into a single summary.
25433
+ */
25434
+ async abstractAtLevel(memories, granularity) {
25435
+ if (granularity === "fine") return memories;
25436
+ const out = [];
25437
+ for (const e of memories) {
25438
+ if (granularity === "medium") {
25439
+ const top3 = pickTop3(e.observations);
25440
+ out.push({ ...e, observations: top3 });
25441
+ } else {
25442
+ const distilled = await this.distill(e.observations, { maxLength: 200 });
25443
+ out.push({ ...e, observations: [distilled.summary] });
25444
+ }
25445
+ }
25446
+ return out;
25447
+ }
25448
+ /**
25449
+ * Compress a working-memory text blob to fit within `maxTokens`.
25450
+ * Delegates to `ContextWindowManager.compressForContext` and chooses
25451
+ * the compression level based on how aggressively we need to shrink.
25452
+ * The `working` parameter is intentionally typed `string` here — the
25453
+ * spec talks about `WorkingMemory` but in practice the compressor
25454
+ * operates on serialized text.
25455
+ */
25456
+ async foldContext(working, maxTokens) {
25457
+ const estTokens = Math.ceil(working.length / 4);
25458
+ if (estTokens <= maxTokens) return working;
25459
+ const ratio = estTokens / maxTokens;
25460
+ const level = ratio > 2 ? "aggressive" : ratio > 1.3 ? "medium" : "light";
25461
+ const result = this.contextWindow.compressForContext(working, { level });
25462
+ return result.compressed;
25463
+ }
25464
+ /**
25465
+ * Identify groups of entities whose observation sets are largely
25466
+ * duplicates. Pairs entities whose union-of-observations Jaccard
25467
+ * exceeds the configured threshold. O(n²) pairwise; suitable for
25468
+ * graphs up to ~1k entities — beyond that, a candidate-blocking
25469
+ * pass on tags/projectId would be the natural extension.
25470
+ *
25471
+ * Algorithmic caveat (greedy single-link): an entity is absorbed
25472
+ * into the FIRST seed it overlaps with above threshold; results
25473
+ * therefore depend on input ordering when an entity would qualify
25474
+ * for multiple seeds. For correctness-critical clustering, use
25475
+ * union-find or complete-link clustering instead.
25476
+ */
25477
+ async findRedundancies(entities) {
25478
+ const groups = [];
25479
+ const visited = /* @__PURE__ */ new Set();
25480
+ for (let i = 0; i < entities.length; i += 1) {
25481
+ if (visited.has(entities[i].name)) continue;
25482
+ const cluster = [entities[i]];
25483
+ const sims = [];
25484
+ visited.add(entities[i].name);
25485
+ for (let j = i + 1; j < entities.length; j += 1) {
25486
+ if (visited.has(entities[j].name)) continue;
25487
+ const sim = jaccard(
25488
+ new Set(entities[i].observations.flatMap((o) => Array.from(tokenize4(o)))),
25489
+ new Set(entities[j].observations.flatMap((o) => Array.from(tokenize4(o))))
25490
+ );
25491
+ if (sim >= this.redundancyThreshold) {
25492
+ cluster.push(entities[j]);
25493
+ sims.push(sim);
25494
+ visited.add(entities[j].name);
25495
+ }
25496
+ }
25497
+ if (cluster.length > 1) {
25498
+ const avg = sims.reduce((a, b) => a + b, 0) / sims.length;
25499
+ groups.push({
25500
+ entities: cluster,
25501
+ canonicalName: cluster[0].name,
25502
+ avgSimilarity: avg
25503
+ });
25504
+ }
25505
+ }
25506
+ return groups;
25507
+ }
25508
+ /**
25509
+ * Collapse a redundancy group into a single canonical entity per
25510
+ * strategy. Doesn't persist — caller is responsible for the actual
25511
+ * `EntityManager.deleteEntities(...)` + `createEntity(merged)` dance
25512
+ * if they want the change durable.
25513
+ */
25514
+ async mergeRedundant(group, strategy) {
25515
+ if (group.entities.length === 0) {
25516
+ throw new Error("TrajectoryCompressor.mergeRedundant: empty group");
25517
+ }
25518
+ let canonical;
25519
+ switch (strategy) {
25520
+ case "keep-newest":
25521
+ canonical = group.entities.reduce((acc, e) => {
25522
+ const epoch = "1970-01-01T00:00:00Z";
25523
+ const accT = Date.parse(acc.lastModified ?? acc.createdAt ?? epoch);
25524
+ const eT = Date.parse(e.lastModified ?? e.createdAt ?? epoch);
25525
+ return Number.isFinite(eT) && eT > (Number.isFinite(accT) ? accT : 0) ? e : acc;
25526
+ });
25527
+ break;
25528
+ case "keep-most-confident":
25529
+ canonical = group.entities.reduce((acc, e) => {
25530
+ const ac = acc.confidence ?? 0;
25531
+ const ec = e.confidence ?? 0;
25532
+ return ec > ac ? e : acc;
25533
+ });
25534
+ break;
25535
+ case "union-observations": {
25536
+ const head = group.entities[0];
25537
+ const allObs = /* @__PURE__ */ new Set();
25538
+ for (const e of group.entities) for (const o of e.observations) allObs.add(o);
25539
+ canonical = { ...head, observations: Array.from(allObs) };
25540
+ break;
25541
+ }
25542
+ }
25543
+ return canonical;
25544
+ }
25545
+ };
25546
+ }
25547
+ });
25548
+
25549
+ // src/agent/ExperienceExtractor.ts
25550
+ function tokenize5(text) {
25551
+ return new Set(
25552
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((t) => t.length > 2)
25553
+ );
25554
+ }
25555
+ function countTokens(texts) {
25556
+ const out = /* @__PURE__ */ new Map();
25557
+ for (const text of texts) {
25558
+ for (const tok of tokenize5(text)) {
25559
+ out.set(tok, (out.get(tok) ?? 0) + 1);
25560
+ }
25561
+ }
25562
+ return out;
25563
+ }
25564
+ function jaccard2(a, b) {
25565
+ if (a.size === 0 && b.size === 0) return 0;
25566
+ let inter = 0;
25567
+ for (const t of a) if (b.has(t)) inter += 1;
25568
+ return inter / (a.size + b.size - inter);
25569
+ }
25570
+ function trajectoryTokens(t, method) {
25571
+ if (method === "structural") {
25572
+ return new Set(t.actions.map((a) => a.name));
25573
+ }
25574
+ const all2 = /* @__PURE__ */ new Set();
25575
+ for (const o of t.observations) for (const tok of tokenize5(o)) all2.add(tok);
25576
+ return all2;
25577
+ }
25578
+ function extractField(t, field) {
25579
+ if (field === "outcome") return t.outcome;
25580
+ return t.context[field];
25581
+ }
25582
+ var ExperienceExtractor;
25583
+ var init_ExperienceExtractor = __esm({
25584
+ "src/agent/ExperienceExtractor.ts"() {
25585
+ "use strict";
25586
+ init_esm_shims();
25587
+ ExperienceExtractor = class {
25588
+ patternDetector;
25589
+ minPatternOccurrences;
25590
+ similarityThreshold;
25591
+ constructor(patternDetector, config = {}) {
25592
+ this.patternDetector = patternDetector;
25593
+ this.minPatternOccurrences = config.minPatternOccurrences ?? 2;
25594
+ this.similarityThreshold = config.similarityThreshold ?? 0.6;
25595
+ }
25596
+ /**
25597
+ * Derive rules from contrastive pairs. Strategy: tokens appearing
25598
+ * disproportionately in successes (vs. failures) become condition
25599
+ * antecedents; the next action after the distinguishing token
25600
+ * becomes the rule's recommended action.
25601
+ *
25602
+ * Lightweight — no embeddings or LLMs. Suitable for the "what does
25603
+ * the agent do differently when it succeeds" question at scale.
25604
+ */
25605
+ async extractFromContrastivePairs(success, failure) {
25606
+ if (success.length === 0 || failure.length === 0) return [];
25607
+ const successTokens = countTokens(success.flatMap((t) => t.observations));
25608
+ const failureTokens = countTokens(failure.flatMap((t) => t.observations));
25609
+ const rules = [];
25610
+ for (const [tok, sCount] of successTokens.entries()) {
25611
+ const fCount = failureTokens.get(tok) ?? 0;
25612
+ if (sCount >= 2 && sCount >= 2 * fCount) {
25613
+ const actionCounts = /* @__PURE__ */ new Map();
25614
+ for (const t of success) {
25615
+ if (t.observations.some((o) => o.toLowerCase().includes(tok))) {
25616
+ for (const a of t.actions) {
25617
+ actionCounts.set(a.name, (actionCounts.get(a.name) ?? 0) + 1);
25618
+ }
25619
+ }
25620
+ }
25621
+ const topAction = Array.from(actionCounts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
25622
+ rules.push({
25623
+ condition: `observation contains "${tok}"`,
25624
+ action: topAction,
25625
+ confidence: sCount / (sCount + fCount + 1),
25626
+ supportCount: sCount,
25627
+ contraCount: fCount
25628
+ });
25629
+ }
25630
+ }
25631
+ return rules.sort((a, b) => b.confidence - a.confidence).slice(0, 10);
25632
+ }
25633
+ /**
25634
+ * Abstract a pattern across trajectories' observations. Delegates
25635
+ * to `PatternDetector.detectPatterns` and lifts the result onto
25636
+ * the spec's `HeuristicGuideline` shape with trajectory provenance.
25637
+ *
25638
+ * `similarityThreshold` is currently unused by the underlying
25639
+ * `detectPatterns` (it operates on token-template equality, not
25640
+ * similarity); kept in the signature for spec compliance and future
25641
+ * use when an embedding-based variant lands.
25642
+ */
25643
+ async abstractPattern(trajectories, similarityThreshold) {
25644
+ void similarityThreshold;
25645
+ const allObs = trajectories.flatMap((t) => t.observations);
25646
+ const patterns = this.patternDetector.detectPatterns(
25647
+ allObs,
25648
+ this.minPatternOccurrences
25649
+ );
25650
+ if (patterns.length === 0) {
25651
+ return { pattern: "", variables: [], occurrences: 0, sourceTrajectoryIds: [] };
25652
+ }
25653
+ const top = patterns.sort((a, b) => b.occurrences - a.occurrences)[0];
25654
+ const sourceIds = /* @__PURE__ */ new Set();
25655
+ const variableValues = new Set(top.variables);
25656
+ for (const t of trajectories) {
25657
+ for (const o of t.observations) {
25658
+ if (Array.from(variableValues).some((v) => o.includes(v))) {
25659
+ sourceIds.add(t.id);
25660
+ break;
25661
+ }
25662
+ }
25663
+ }
25664
+ return {
25665
+ pattern: top.pattern,
25666
+ variables: top.variables ?? [],
25667
+ occurrences: top.occurrences,
25668
+ sourceTrajectoryIds: Array.from(sourceIds)
25669
+ };
25670
+ }
25671
+ /**
25672
+ * Learn the decision boundary for a binary outcome split. Currently
25673
+ * supports `outcome` field (success vs. failure); other field names
25674
+ * fall back to the `Outcome` lookup. Returns the most-distinguishing
25675
+ * tokens per side.
25676
+ */
25677
+ async learnDecisionBoundary(trajectories, outcomeField) {
25678
+ const positive = trajectories.filter(
25679
+ (t) => extractField(t, outcomeField) === "success"
25680
+ );
25681
+ const negative = trajectories.filter(
25682
+ (t) => extractField(t, outcomeField) === "failure"
25683
+ );
25684
+ const posTokens = countTokens(positive.flatMap((t) => t.observations));
25685
+ const negTokens = countTokens(negative.flatMap((t) => t.observations));
25686
+ const presence = [];
25687
+ const absence = [];
25688
+ for (const [tok, p] of posTokens) {
25689
+ const n = negTokens.get(tok) ?? 0;
25690
+ if (p >= 2 && p >= 2 * n) presence.push(tok);
25691
+ }
25692
+ for (const [tok, n] of negTokens) {
25693
+ const p = posTokens.get(tok) ?? 0;
25694
+ if (n >= 2 && n >= 2 * p) absence.push(tok);
25695
+ }
25696
+ const total = positive.length + negative.length;
25697
+ return {
25698
+ presenceTokens: presence.slice(0, 10),
25699
+ absenceTokens: absence.slice(0, 10),
25700
+ outcomeIfPresent: "success",
25701
+ outcomeIfAbsent: "failure",
25702
+ confidence: total === 0 ? 0 : Math.min(positive.length, negative.length) / total
25703
+ };
25704
+ }
25705
+ /**
25706
+ * Cluster trajectories by the chosen method. Lightweight: no
25707
+ * embeddings — `semantic` and `structural` both use token-Jaccard
25708
+ * with different normalization; `outcome` simply groups by the
25709
+ * `Outcome` value.
25710
+ *
25711
+ * Algorithmic caveat (greedy single-link for semantic/structural):
25712
+ * a trajectory is absorbed into the FIRST seed it overlaps with
25713
+ * above the configured similarity threshold, regardless of whether
25714
+ * a later seed would match more strongly. Results therefore depend
25715
+ * on input ordering, and "chain" clusters (A↔B, B↔C, but A↔C far
25716
+ * apart) can form under low thresholds. The `cohesion` field on
25717
+ * each `TrajectoryCluster` surfaces this — downstream
25718
+ * `synthesizeExperience` already passes cohesion through to
25719
+ * `Experience.confidence`. For higher-quality clustering, a
25720
+ * complete-link or union-find variant would be the natural
25721
+ * extension.
25722
+ */
25723
+ async clusterTrajectories(trajectories, method) {
25724
+ if (trajectories.length === 0) return [];
25725
+ if (method === "outcome") {
25726
+ const groups = /* @__PURE__ */ new Map();
25727
+ for (const t of trajectories) {
25728
+ const arr = groups.get(t.outcome) ?? [];
25729
+ arr.push(t);
25730
+ groups.set(t.outcome, arr);
25731
+ }
25732
+ return Array.from(groups.entries()).map(([outcome, ts], i) => ({
25733
+ id: `cluster-outcome-${i}-${outcome}`,
25734
+ method: "outcome",
25735
+ trajectories: ts,
25736
+ cohesion: 1
25737
+ // outcome equality = perfect cohesion by definition
25738
+ }));
25739
+ }
25740
+ const clusters = [];
25741
+ const sims = [];
25742
+ const visited = /* @__PURE__ */ new Set();
25743
+ for (const t of trajectories) {
25744
+ if (visited.has(t.id)) continue;
25745
+ const cluster = [t];
25746
+ const cSims = [];
25747
+ visited.add(t.id);
25748
+ const tTokens = trajectoryTokens(t, method);
25749
+ for (const u of trajectories) {
25750
+ if (visited.has(u.id)) continue;
25751
+ const sim = jaccard2(tTokens, trajectoryTokens(u, method));
25752
+ if (sim >= this.similarityThreshold) {
25753
+ cluster.push(u);
25754
+ cSims.push(sim);
25755
+ visited.add(u.id);
25756
+ }
25757
+ }
25758
+ clusters.push(cluster);
25759
+ sims.push(cSims);
25760
+ }
25761
+ return clusters.map((cluster, i) => ({
25762
+ id: `cluster-${method}-${i}`,
25763
+ method,
25764
+ trajectories: cluster,
25765
+ cohesion: sims[i].length === 0 ? 1 : sims[i].reduce((a, b) => a + b, 0) / sims[i].length
25766
+ }));
25767
+ }
25768
+ /**
25769
+ * Synthesize a transferable `Experience` from a cluster. Picks the
25770
+ * `type` heuristically (procedure if cluster is action-heavy;
25771
+ * heuristic otherwise) and uses the most-common-pattern across the
25772
+ * cluster as the experience content.
25773
+ */
25774
+ async synthesizeExperience(cluster) {
25775
+ const id = `exp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
25776
+ const obs = cluster.trajectories.flatMap((t) => t.observations);
25777
+ const actionCount = cluster.trajectories.reduce((acc, t) => acc + t.actions.length, 0);
25778
+ const type = actionCount > cluster.trajectories.length * 2 ? "procedure" : "heuristic";
25779
+ const patterns = this.patternDetector.detectPatterns(obs, 2);
25780
+ const content = patterns[0]?.pattern ?? obs[0] ?? "";
25781
+ const counts = countTokens(obs);
25782
+ const applicability = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t]) => t);
25783
+ return {
25784
+ id,
25785
+ type,
25786
+ content,
25787
+ applicability,
25788
+ confidence: cluster.cohesion,
25789
+ sourceTrajectories: cluster.trajectories.map((t) => t.id),
25790
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
25791
+ };
25792
+ }
25793
+ };
25794
+ }
25795
+ });
25796
+
25797
+ // src/agent/procedural/ProcedureStore.ts
25798
+ function encodeObservations(procedure) {
25799
+ const observations = [];
25800
+ if (procedure.description && procedure.description.trim() !== "") {
25801
+ observations.push(procedure.description);
25802
+ }
25803
+ observations.push(`${STEPS_PREFIX}${JSON.stringify(procedure.steps)}`);
25804
+ observations.push(
25805
+ `${META_PREFIX}${JSON.stringify({
25806
+ triggers: procedure.triggers ?? [],
25807
+ successRate: procedure.successRate ?? 0,
25808
+ executionCount: procedure.executionCount ?? 0
25809
+ })}`
25810
+ );
25811
+ return observations;
25812
+ }
25813
+ function decodeProcedure(id, observations) {
25814
+ let steps = [];
25815
+ let triggers = [];
25816
+ let successRate = 0;
25817
+ let executionCount = 0;
25818
+ const descriptionLines = [];
25819
+ for (const obs of observations) {
25820
+ if (obs.startsWith(STEPS_PREFIX)) {
25821
+ try {
25822
+ const parsed = JSON.parse(obs.slice(STEPS_PREFIX.length));
25823
+ if (Array.isArray(parsed)) steps = parsed;
25824
+ } catch {
25825
+ }
25826
+ } else if (obs.startsWith(META_PREFIX)) {
25827
+ try {
25828
+ const parsed = JSON.parse(obs.slice(META_PREFIX.length));
25829
+ triggers = parsed.triggers ?? [];
25830
+ successRate = parsed.successRate ?? 0;
25831
+ executionCount = parsed.executionCount ?? 0;
25832
+ } catch {
25833
+ }
25834
+ } else {
25835
+ descriptionLines.push(obs);
25836
+ }
25837
+ }
25838
+ return {
25839
+ id,
25840
+ name: id,
25841
+ description: descriptionLines.join("\n"),
25842
+ steps,
25843
+ triggers,
25844
+ successRate,
25845
+ executionCount
25846
+ };
25847
+ }
25848
+ var STEPS_PREFIX, META_PREFIX, PROCEDURE_ENTITY_TYPE, ProcedureStore;
25849
+ var init_ProcedureStore = __esm({
25850
+ "src/agent/procedural/ProcedureStore.ts"() {
25851
+ "use strict";
25852
+ init_esm_shims();
25853
+ STEPS_PREFIX = "[procedure-steps]:";
25854
+ META_PREFIX = "[procedure-meta]:";
25855
+ PROCEDURE_ENTITY_TYPE = "procedure";
25856
+ ProcedureStore = class {
25857
+ constructor(entityManager) {
25858
+ this.entityManager = entityManager;
25859
+ }
25860
+ /**
25861
+ * Persist a new procedure. The entity name = `procedure.id`. Steps and
25862
+ * metadata are encoded as JSON observations alongside any caller-supplied
25863
+ * description observation. Idempotent on duplicate id (relies on
25864
+ * `EntityManager.createEntities` semantics).
25865
+ */
25866
+ async save(procedure) {
25867
+ const observations = encodeObservations(procedure);
25868
+ await this.entityManager.createEntities([
25869
+ {
25870
+ name: procedure.id,
25871
+ entityType: PROCEDURE_ENTITY_TYPE,
25872
+ observations,
25873
+ tags: ["procedure", ...procedure.triggers ?? []]
25874
+ }
25875
+ ]);
25876
+ }
25877
+ /**
25878
+ * Load a procedure by id, or null if not found. Tolerant of partial
25879
+ * encodings — steps default to `[]`, meta to zeroed fields.
25880
+ */
25881
+ async load(id) {
25882
+ const entity = await this.entityManager.getEntity(id);
25883
+ if (!entity || entity.entityType !== PROCEDURE_ENTITY_TYPE) return null;
25884
+ return decodeProcedure(id, entity.observations);
25885
+ }
25886
+ /**
25887
+ * Replace an existing procedure's steps + metadata. Throws if the
25888
+ * entity doesn't exist or isn't a procedure.
25889
+ */
25890
+ async update(procedure) {
25891
+ await this.entityManager.updateEntity(procedure.id, {
25892
+ observations: encodeObservations(procedure),
25893
+ tags: ["procedure", ...procedure.triggers ?? []]
25894
+ });
25895
+ }
25896
+ };
25897
+ }
25898
+ });
25899
+
25900
+ // src/agent/procedural/StepSequencer.ts
25901
+ var StepSequencer;
25902
+ var init_StepSequencer = __esm({
25903
+ "src/agent/procedural/StepSequencer.ts"() {
25904
+ "use strict";
25905
+ init_esm_shims();
25906
+ StepSequencer = class {
25907
+ constructor(procedure) {
25908
+ this.procedure = procedure;
25909
+ }
25910
+ cursor = 0;
25911
+ /** When set, all `current()` / `next()` calls return this fallback chain
25912
+ * instead of the main steps until cleared. */
25913
+ activeFallback = null;
25914
+ /** Steps in 1-indexed order. Read-only. */
25915
+ get steps() {
25916
+ return this.procedure.steps;
25917
+ }
25918
+ /** Index of the next step to execute (0-based). Public for tests. */
25919
+ get cursorIndex() {
25920
+ return this.cursor;
25921
+ }
25922
+ /** Whether all main-track steps have been consumed. Fallbacks may still run. */
25923
+ isComplete() {
25924
+ return this.activeFallback === null && this.cursor >= this.procedure.steps.length;
25925
+ }
25926
+ /** Return the step about to execute, or null if exhausted. */
25927
+ current() {
25928
+ if (this.activeFallback) return this.activeFallback;
25929
+ return this.procedure.steps[this.cursor] ?? null;
25930
+ }
25931
+ /**
25932
+ * Advance the cursor and return the new current step (or null when
25933
+ * complete). Clears any active fallback — fallbacks are single-step
25934
+ * by design; deeper branching needs a nested fallback in the step's
25935
+ * `fallback.fallback`.
25936
+ */
25937
+ next() {
25938
+ if (this.activeFallback) {
25939
+ this.activeFallback = null;
25940
+ this.cursor++;
25941
+ return this.current();
25942
+ }
25943
+ this.cursor++;
25944
+ return this.current();
25945
+ }
25946
+ /**
25947
+ * Switch to the current step's `fallback` chain. The next `current()`
25948
+ * call will return the fallback's first step. Throws if the current
25949
+ * step has no fallback (caller should test before invoking).
25950
+ */
25951
+ branchToFallback() {
25952
+ const step = this.current();
25953
+ if (!step?.fallback) {
25954
+ throw new Error(`Step ${step?.order ?? "?"} has no fallback`);
25955
+ }
25956
+ this.activeFallback = step.fallback;
25957
+ }
25958
+ /** Reset the cursor and clear any fallback — start over. */
25959
+ reset() {
25960
+ this.cursor = 0;
25961
+ this.activeFallback = null;
25962
+ }
25963
+ };
25964
+ }
25965
+ });
25966
+
25967
+ // src/agent/procedural/ProcedureManager.ts
25968
+ import { randomUUID as randomUUID2 } from "crypto";
25969
+ function tokenize6(s) {
25970
+ return new Set(
25971
+ s.toLowerCase().split(/[^a-z0-9]+/g).filter((t) => t.length >= 2)
25972
+ );
25973
+ }
25974
+ function clamp01(x) {
25975
+ return Math.max(0, Math.min(1, x));
25976
+ }
25977
+ var ProcedureManager;
25978
+ var init_ProcedureManager = __esm({
25979
+ "src/agent/procedural/ProcedureManager.ts"() {
25980
+ "use strict";
25981
+ init_esm_shims();
25982
+ init_ProcedureStore();
25983
+ init_StepSequencer();
25984
+ ProcedureManager = class {
25985
+ store;
25986
+ successRateAlpha;
25987
+ constructor(entityManager, config = {}) {
25988
+ this.store = new ProcedureStore(entityManager);
25989
+ this.successRateAlpha = config.successRateAlpha ?? 0.2;
25990
+ }
25991
+ /**
25992
+ * Persist a new procedure. Auto-generates `id` (and falls back to it
25993
+ * for `name`) when caller omits them. Throws if the id collides — the
25994
+ * caller should `getProcedure` first if upsert semantics are needed.
25995
+ */
25996
+ async addProcedure(input) {
25997
+ if (!input.steps) {
25998
+ throw new Error("addProcedure: steps[] is required");
25999
+ }
26000
+ const id = input.id ?? `proc-${randomUUID2()}`;
26001
+ const procedure = {
26002
+ id,
26003
+ name: input.name ?? id,
26004
+ description: input.description ?? "",
26005
+ steps: input.steps,
26006
+ triggers: input.triggers ?? [],
26007
+ successRate: input.successRate ?? 0,
26008
+ executionCount: input.executionCount ?? 0,
26009
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
26010
+ lastModified: (/* @__PURE__ */ new Date()).toISOString()
26011
+ };
26012
+ await this.store.save(procedure);
26013
+ return procedure;
26014
+ }
26015
+ /** Load by id, or null. */
26016
+ async getProcedure(id) {
26017
+ return this.store.load(id);
26018
+ }
26019
+ /**
26020
+ * Stateless lookup of a specific step by 1-indexed `stepOrder`. Returns
26021
+ * null when the procedure has no such step.
26022
+ */
26023
+ async getStep(procedureId, stepOrder) {
26024
+ const proc = await this.getProcedure(procedureId);
26025
+ if (!proc) return null;
26026
+ return proc.steps.find((s) => s.order === stepOrder) ?? null;
26027
+ }
26028
+ /**
26029
+ * Look up the step that follows `currentOrder`. Returns null when at
26030
+ * the end of the procedure or the procedure does not exist.
26031
+ */
26032
+ async getNextStep(procedureId, currentOrder) {
26033
+ const proc = await this.getProcedure(procedureId);
26034
+ if (!proc) return null;
26035
+ const idx = proc.steps.findIndex((s) => s.order === currentOrder);
26036
+ if (idx < 0 || idx + 1 >= proc.steps.length) return null;
26037
+ return proc.steps[idx + 1];
26038
+ }
26039
+ /**
26040
+ * Open a fresh `StepSequencer` for the named procedure. Returns null
26041
+ * when the procedure doesn't exist. Multiple sequencers per procedure
26042
+ * are independent — no shared cursor state.
26043
+ */
26044
+ async openSequencer(procedureId) {
26045
+ const proc = await this.getProcedure(procedureId);
26046
+ if (!proc) return null;
26047
+ return new StepSequencer(proc);
26048
+ }
26049
+ /**
26050
+ * Token-overlap match: scores each procedure by Jaccard-like overlap
26051
+ * between the lowercased context tokens and the union of (`name`,
26052
+ * `triggers`). Returns matches in score order, descending. `threshold`
26053
+ * filters out matches below the cutoff (default 0.0 — return all).
26054
+ */
26055
+ async matchProcedure(contextDescription, candidates, threshold = 0) {
26056
+ const ctxTokens = tokenize6(contextDescription);
26057
+ if (ctxTokens.size === 0) return [];
26058
+ const matches = [];
26059
+ for (const procedure of candidates) {
26060
+ const procTokens = /* @__PURE__ */ new Set();
26061
+ for (const t of tokenize6(procedure.name)) procTokens.add(t);
26062
+ for (const trig of procedure.triggers ?? []) {
26063
+ for (const t of tokenize6(trig)) procTokens.add(t);
26064
+ }
26065
+ if (procTokens.size === 0) continue;
26066
+ const intersection = /* @__PURE__ */ new Set();
26067
+ for (const t of ctxTokens) if (procTokens.has(t)) intersection.add(t);
26068
+ const union = /* @__PURE__ */ new Set([...ctxTokens, ...procTokens]);
26069
+ const score = intersection.size / union.size;
26070
+ if (score >= threshold) matches.push({ procedure, score });
26071
+ }
26072
+ matches.sort((a, b) => b.score - a.score);
26073
+ return matches;
26074
+ }
26075
+ /**
26076
+ * Apply caller feedback: increment `executionCount` and update
26077
+ * `successRate` via EWMA. Persists the updated procedure. Returns
26078
+ * the updated record. Throws if procedure does not exist.
26079
+ */
26080
+ async refineProcedure(procedureId, feedback) {
26081
+ const proc = await this.getProcedure(procedureId);
26082
+ if (!proc) {
26083
+ throw new Error(`Procedure '${procedureId}' not found`);
26084
+ }
26085
+ const previousRate = proc.successRate ?? 0;
26086
+ const observation = feedback.succeeded ? 1 : 0;
26087
+ const baseline = (proc.executionCount ?? 0) === 0 ? 0.5 : previousRate;
26088
+ const newRate = baseline + this.successRateAlpha * (observation - baseline);
26089
+ const updated = {
26090
+ ...proc,
26091
+ successRate: clamp01(newRate),
26092
+ executionCount: (proc.executionCount ?? 0) + 1,
26093
+ lastModified: feedback.recordedAt ?? (/* @__PURE__ */ new Date()).toISOString()
26094
+ };
26095
+ await this.store.update(updated);
26096
+ return updated;
26097
+ }
26098
+ };
26099
+ }
26100
+ });
26101
+
26102
+ // src/agent/causal/CausalReasoner.ts
26103
+ function chainScore(relations) {
26104
+ let score = 1;
26105
+ for (const r of relations) {
26106
+ const meta = r.metadata;
26107
+ const strength = typeof meta?.causalStrength === "number" ? meta.causalStrength : 1;
26108
+ score *= strength;
26109
+ }
26110
+ return score;
26111
+ }
26112
+ var DEFAULT_CAUSAL_RELATION_TYPES, CausalReasoner;
26113
+ var init_CausalReasoner = __esm({
26114
+ "src/agent/causal/CausalReasoner.ts"() {
26115
+ "use strict";
26116
+ init_esm_shims();
26117
+ DEFAULT_CAUSAL_RELATION_TYPES = [
26118
+ "causes",
26119
+ "enables",
26120
+ "prevents",
26121
+ "precedes",
26122
+ "correlates"
26123
+ ];
26124
+ CausalReasoner = class {
26125
+ constructor(traversal, config = {}) {
26126
+ this.traversal = traversal;
26127
+ this.causalTypes = config.causalTypes ?? DEFAULT_CAUSAL_RELATION_TYPES;
26128
+ this.maxDepth = config.maxDepth ?? 6;
26129
+ }
26130
+ causalTypes;
26131
+ maxDepth;
26132
+ /**
26133
+ * Find all causal chains ending at `effectEntityName`. Searches for
26134
+ * paths from any node to `effectEntityName` along causal edges. In
26135
+ * practice we delegate to `findAllPaths` per candidate cause; for
26136
+ * unbounded discovery the caller should layer their own seed selection.
26137
+ *
26138
+ * Returns an empty array when no causal chain reaches the target. Each
26139
+ * chain's `score` is the product of `causalStrength` annotations on
26140
+ * its relations (defaults to 1 per edge when missing).
26141
+ */
26142
+ async findCauses(effectEntityName, candidateCauses, maxDepth) {
26143
+ const depth = maxDepth ?? this.maxDepth;
26144
+ const chains = [];
26145
+ for (const cause of candidateCauses) {
26146
+ const paths = await this.traversal.findAllPaths(cause, effectEntityName, depth, {
26147
+ relationTypes: this.causalTypes
26148
+ });
26149
+ for (const p of paths) {
26150
+ chains.push({
26151
+ path: p.path,
26152
+ relations: p.relations,
26153
+ score: chainScore(p.relations),
26154
+ length: p.relations.length
26155
+ });
26156
+ }
26157
+ }
26158
+ chains.sort((a, b) => b.score - a.score);
26159
+ return chains;
26160
+ }
26161
+ /**
26162
+ * Find all causal chains starting at `causeEntityName` and reaching
26163
+ * any of `candidateEffects`. Symmetric counterpart to `findCauses`.
26164
+ */
26165
+ async findEffects(causeEntityName, candidateEffects, maxDepth) {
26166
+ const depth = maxDepth ?? this.maxDepth;
26167
+ const chains = [];
26168
+ for (const effect of candidateEffects) {
26169
+ const paths = await this.traversal.findAllPaths(causeEntityName, effect, depth, {
26170
+ relationTypes: this.causalTypes
26171
+ });
26172
+ for (const p of paths) {
26173
+ chains.push({
26174
+ path: p.path,
26175
+ relations: p.relations,
26176
+ score: chainScore(p.relations),
26177
+ length: p.relations.length
26178
+ });
26179
+ }
26180
+ }
26181
+ chains.sort((a, b) => b.score - a.score);
26182
+ return chains;
26183
+ }
26184
+ /**
26185
+ * Counterfactual: "what changes if we remove edge `(removeFrom →
26186
+ * removeTo)` and ask whether `predict` is still reachable from
26187
+ * `seed`?" Returns chains from `seed` to `predict` that DO NOT use
26188
+ * the removed edge. Compare against the unfiltered `findEffects`
26189
+ * result to see which chains the removal kills.
26190
+ *
26191
+ * Pure: does not mutate the underlying graph or storage.
26192
+ */
26193
+ async counterfactual(scenario) {
26194
+ const depth = scenario.maxDepth ?? this.maxDepth;
26195
+ const paths = await this.traversal.findAllPaths(
26196
+ scenario.seed,
26197
+ scenario.predict,
26198
+ depth,
26199
+ { relationTypes: this.causalTypes }
26200
+ );
26201
+ const surviving = paths.filter(
26202
+ (p) => !p.relations.some(
26203
+ (r) => r.from === scenario.removeFrom && r.to === scenario.removeTo
26204
+ )
26205
+ );
26206
+ return surviving.map((p) => ({
26207
+ path: p.path,
26208
+ relations: p.relations,
26209
+ score: chainScore(p.relations),
26210
+ length: p.relations.length
26211
+ }));
26212
+ }
26213
+ /**
26214
+ * Detect cycles in the causal subgraph rooted at `seed`. Returns each
26215
+ * cycle as a list of entity names (with the repeating node at both
26216
+ * ends) plus the edges that close the loop.
26217
+ *
26218
+ * **Caveat**: treats `prevents` as a directed causal edge, NOT as a
26219
+ * negation. A `prevents`→`enables`→`prevents` triangle WILL show up
26220
+ * as a cycle. Document explicitly so callers don't misinterpret.
26221
+ *
26222
+ * Cycle detection here is a depth-bounded DFS rather than full Tarjan
26223
+ * SCC — sufficient for sparse causal graphs at hop counts ≤ 6, but
26224
+ * may double-report cycles that share edges. Filter by `cycle[0]`
26225
+ * sort-then-stringify if exact dedup is needed.
26226
+ */
26227
+ detectCycles(seed, maxDepth) {
26228
+ const depth = maxDepth ?? this.maxDepth;
26229
+ const cycles = [];
26230
+ const path6 = [];
26231
+ const relations = [];
26232
+ const inPath = /* @__PURE__ */ new Set();
26233
+ const dfs = (node, d) => {
26234
+ if (d > depth) return;
26235
+ inPath.add(node);
26236
+ path6.push(node);
26237
+ const neighbors = this.traversal.getNeighborsWithRelations(node, {
26238
+ relationTypes: this.causalTypes,
26239
+ direction: "outgoing"
26240
+ });
26241
+ for (const { neighbor, relation } of neighbors) {
26242
+ if (inPath.has(neighbor)) {
26243
+ const idx = path6.indexOf(neighbor);
26244
+ if (idx >= 0) {
26245
+ cycles.push({
26246
+ cycle: [...path6.slice(idx), neighbor],
26247
+ relations: [...relations.slice(idx), relation]
26248
+ });
26249
+ }
26250
+ } else {
26251
+ relations.push(relation);
26252
+ dfs(neighbor, d + 1);
26253
+ relations.pop();
26254
+ }
26255
+ }
26256
+ inPath.delete(node);
26257
+ path6.pop();
26258
+ };
26259
+ dfs(seed, 0);
26260
+ return cycles;
26261
+ }
26262
+ };
26263
+ }
26264
+ });
26265
+
26266
+ // src/agent/rbac/PermissionMatrix.ts
26267
+ function permissionsForRole(role, resourceType, matrix = DEFAULT_PERMISSION_MATRIX, overrides) {
26268
+ const overrideMatrix = overrides?.get(resourceType);
26269
+ if (overrideMatrix?.has(role)) {
26270
+ return overrideMatrix.get(role);
26271
+ }
26272
+ return matrix.get(role) ?? [];
26273
+ }
26274
+ var DEFAULT_PERMISSION_MATRIX;
26275
+ var init_PermissionMatrix = __esm({
26276
+ "src/agent/rbac/PermissionMatrix.ts"() {
26277
+ "use strict";
26278
+ init_esm_shims();
26279
+ DEFAULT_PERMISSION_MATRIX = /* @__PURE__ */ new Map([
26280
+ ["reader", ["read"]],
26281
+ ["writer", ["read", "write"]],
26282
+ ["admin", ["read", "write", "delete"]],
26283
+ ["owner", ["read", "write", "delete", "manage"]]
26284
+ ]);
26285
+ }
26286
+ });
26287
+
26288
+ // src/agent/rbac/RbacMiddleware.ts
26289
+ var RbacMiddleware;
26290
+ var init_RbacMiddleware = __esm({
26291
+ "src/agent/rbac/RbacMiddleware.ts"() {
26292
+ "use strict";
26293
+ init_esm_shims();
26294
+ init_PermissionMatrix();
26295
+ RbacMiddleware = class {
26296
+ constructor(store, options) {
26297
+ this.store = store;
26298
+ this.matrix = options?.matrix ?? DEFAULT_PERMISSION_MATRIX;
26299
+ this.overrides = options?.overrides;
26300
+ this.defaultRole = options?.defaultRole === void 0 && options !== void 0 ? options.defaultRole : options?.defaultRole ?? "reader";
26301
+ }
26302
+ matrix;
26303
+ overrides;
26304
+ defaultRole;
26305
+ checkPermission(agentId, action, resourceType, resourceName, now) {
26306
+ const active = this.store.listActive(agentId, now);
26307
+ const applicable = active.filter((a) => this.matchesResource(a, resourceType, resourceName));
26308
+ if (applicable.length === 0) {
26309
+ if (!this.defaultRole) return false;
26310
+ const granted = permissionsForRole(this.defaultRole, resourceType, this.matrix, this.overrides);
26311
+ return granted.includes(action);
26312
+ }
26313
+ return applicable.some((a) => {
26314
+ const granted = permissionsForRole(a.role, resourceType, this.matrix, this.overrides);
26315
+ return granted.includes(action);
26316
+ });
26317
+ }
26318
+ matchesResource(assignment, resourceType, resourceName) {
26319
+ if (assignment.resourceType !== void 0 && assignment.resourceType !== resourceType) {
26320
+ return false;
26321
+ }
26322
+ if (assignment.scope) {
26323
+ if (!resourceName) return false;
26324
+ if (!resourceName.startsWith(assignment.scope)) return false;
26325
+ }
26326
+ return true;
26327
+ }
26328
+ };
26329
+ }
26330
+ });
26331
+
26332
+ // src/agent/rbac/RoleAssignmentStore.ts
26333
+ import { promises as fs9 } from "fs";
26334
+ var RoleAssignmentStore;
26335
+ var init_RoleAssignmentStore = __esm({
26336
+ "src/agent/rbac/RoleAssignmentStore.ts"() {
26337
+ "use strict";
26338
+ init_esm_shims();
26339
+ RoleAssignmentStore = class {
26340
+ assignments = /* @__PURE__ */ new Map();
26341
+ persistencePath;
26342
+ constructor(options) {
26343
+ this.persistencePath = options?.persistencePath;
26344
+ }
26345
+ /**
26346
+ * Replay the JSONL persistence file (if configured) into the in-memory
26347
+ * map. Idempotent — safe to call multiple times. No-op when no path
26348
+ * is set or the file does not exist.
26349
+ */
26350
+ async hydrate() {
26351
+ if (!this.persistencePath) return;
26352
+ let content;
26353
+ try {
26354
+ content = await fs9.readFile(this.persistencePath, "utf-8");
26355
+ } catch (e) {
26356
+ if (e.code === "ENOENT") return;
26357
+ throw e;
26358
+ }
26359
+ this.assignments.clear();
26360
+ for (const line of content.split("\n")) {
26361
+ if (!line.trim()) continue;
26362
+ try {
26363
+ const rec = JSON.parse(line);
26364
+ if (rec.op === "assign") {
26365
+ this.applyAssign(rec.assignment);
26366
+ } else {
26367
+ this.applyRevoke(rec.agentId, rec.role, rec.resourceType);
26368
+ }
26369
+ } catch {
26370
+ }
26371
+ }
26372
+ }
26373
+ /**
26374
+ * Add an assignment. Multiple grants per agent are allowed (e.g. one
26375
+ * agent may be a `reader` for entities and a `writer` for relations).
26376
+ * Persists if configured.
26377
+ */
26378
+ async assign(assignment) {
26379
+ this.applyAssign(assignment);
26380
+ await this.persist({ op: "assign", assignment, ts: (/* @__PURE__ */ new Date()).toISOString() });
26381
+ }
26382
+ /**
26383
+ * Remove a specific assignment. Matching is by `agentId + role +
26384
+ * resourceType` (the resourceType match is exact, including undefined).
26385
+ */
26386
+ async revoke(agentId, role, resourceType) {
26387
+ this.applyRevoke(agentId, role, resourceType);
26388
+ await this.persist({ op: "revoke", agentId, role, resourceType, ts: (/* @__PURE__ */ new Date()).toISOString() });
26389
+ }
26390
+ /** All assignments for the given agent (active and inactive). */
26391
+ list(agentId) {
26392
+ return this.assignments.get(agentId)?.slice() ?? [];
26393
+ }
26394
+ /**
26395
+ * Active assignments for the given agent at the supplied time. Default
26396
+ * is current time. An assignment is active when `validFrom <= now <=
26397
+ * validUntil` (with absent bounds treated as unbounded).
26398
+ */
26399
+ listActive(agentId, now) {
26400
+ const ts = now ?? (/* @__PURE__ */ new Date()).toISOString();
26401
+ return this.list(agentId).filter((a) => {
26402
+ if (a.validFrom && a.validFrom > ts) return false;
26403
+ if (a.validUntil && a.validUntil < ts) return false;
26404
+ return true;
26405
+ });
26406
+ }
26407
+ // -------- Internal --------
26408
+ applyAssign(assignment) {
26409
+ const list = this.assignments.get(assignment.agentId) ?? [];
26410
+ list.push(assignment);
26411
+ this.assignments.set(assignment.agentId, list);
26412
+ }
26413
+ applyRevoke(agentId, role, resourceType) {
26414
+ const list = this.assignments.get(agentId);
26415
+ if (!list) return;
26416
+ const filtered = list.filter(
26417
+ (a) => !(a.role === role && a.resourceType === resourceType)
26418
+ );
26419
+ if (filtered.length === 0) {
26420
+ this.assignments.delete(agentId);
26421
+ } else {
26422
+ this.assignments.set(agentId, filtered);
26423
+ }
26424
+ }
26425
+ async persist(record) {
26426
+ if (!this.persistencePath) return;
26427
+ const line = JSON.stringify(record) + "\n";
26428
+ await fs9.appendFile(this.persistencePath, line, "utf-8");
26429
+ }
26430
+ };
26431
+ }
26432
+ });
26433
+
26434
+ // src/agent/world/WorldStateSnapshot.ts
26435
+ function diffFields(before, after) {
26436
+ const out = [];
26437
+ if (before.entityType !== after.entityType) out.push("entityType");
26438
+ if (before.importance !== after.importance) out.push("importance");
26439
+ if (before.confidence !== after.confidence) out.push("confidence");
26440
+ if (before.observationCount !== after.observationCount) out.push("observationCount");
26441
+ if (!sameStringSet(before.tags, after.tags)) out.push("tags");
26442
+ if (before.lastModified !== after.lastModified) out.push("lastModified");
26443
+ return out;
26444
+ }
26445
+ function sameStringSet(a, b) {
26446
+ if (a.length !== b.length) return false;
26447
+ const setA = new Set(a);
26448
+ for (const x of b) if (!setA.has(x)) return false;
26449
+ return true;
26450
+ }
26451
+ var WorldStateSnapshot;
26452
+ var init_WorldStateSnapshot = __esm({
26453
+ "src/agent/world/WorldStateSnapshot.ts"() {
26454
+ "use strict";
26455
+ init_esm_shims();
26456
+ WorldStateSnapshot = class _WorldStateSnapshot {
26457
+ /** ISO 8601 timestamp this snapshot was taken. */
26458
+ takenAt;
26459
+ /** Map keyed by entity name. */
26460
+ entitiesByName;
26461
+ constructor(entities, takenAt) {
26462
+ this.takenAt = takenAt ?? (/* @__PURE__ */ new Date()).toISOString();
26463
+ const m = /* @__PURE__ */ new Map();
26464
+ for (const e of entities) m.set(e.name, e);
26465
+ this.entitiesByName = m;
26466
+ }
26467
+ /** Number of entities in the snapshot. */
26468
+ get size() {
26469
+ return this.entitiesByName.size;
26470
+ }
26471
+ /** All entities, in insertion order. */
26472
+ entities() {
26473
+ return [...this.entitiesByName.values()];
26474
+ }
26475
+ /**
26476
+ * Pure: compute the diff to a `next` snapshot. Returns added /
26477
+ * removed / modified breakdown. An entity counts as "modified" when
26478
+ * any of `importance`, `confidence`, `observationCount`, `tags`, or
26479
+ * `lastModified` differs.
26480
+ */
26481
+ diffTo(next) {
26482
+ const removed = [];
26483
+ const added = [];
26484
+ const modified = [];
26485
+ for (const [name, before] of this.entitiesByName) {
26486
+ const after = next.entitiesByName.get(name);
26487
+ if (!after) {
26488
+ removed.push(before);
26489
+ continue;
26490
+ }
26491
+ const fields = diffFields(before, after);
26492
+ if (fields.length > 0) {
26493
+ modified.push({ name, before, after, fields });
26494
+ }
26495
+ }
26496
+ for (const [name, after] of next.entitiesByName) {
26497
+ if (!this.entitiesByName.has(name)) added.push(after);
26498
+ }
26499
+ return { removed, added, modified };
26500
+ }
26501
+ /** JSON-serializable form. */
26502
+ toJSON() {
26503
+ return { takenAt: this.takenAt, entities: [...this.entities()] };
26504
+ }
26505
+ /** Reconstruct from `toJSON()` output. */
26506
+ static fromJSON(json) {
26507
+ return new _WorldStateSnapshot(json.entities, json.takenAt);
26508
+ }
26509
+ };
26510
+ }
26511
+ });
26512
+
26513
+ // src/agent/world/WorldModelManager.ts
26514
+ var WorldModelManager;
26515
+ var init_WorldModelManager = __esm({
26516
+ "src/agent/world/WorldModelManager.ts"() {
26517
+ "use strict";
26518
+ init_esm_shims();
26519
+ init_WorldStateSnapshot();
26520
+ WorldModelManager = class {
26521
+ constructor(entityManager, causalReasoner, memoryValidator, options = {}) {
26522
+ this.entityManager = entityManager;
26523
+ this.causalReasoner = causalReasoner;
26524
+ this.memoryValidator = memoryValidator;
26525
+ this.maxSnapshotSize = options.maxSnapshotSize ?? 1e3;
26526
+ }
26527
+ maxSnapshotSize;
26528
+ /**
26529
+ * Build a fresh snapshot from the live graph. Loads ALL entities (capped
26530
+ * at `maxSnapshotSize`) and reduces each to a `WorldStateEntity`. Pure
26531
+ * reads; safe to call concurrently.
26532
+ *
26533
+ * For graphs larger than the cap, entities are sorted by `importance`
26534
+ * descending and truncated — high-importance entities preferred.
26535
+ */
26536
+ async getCurrentState() {
26537
+ const graph = await this.entityManager["storage"].loadGraph();
26538
+ let entities = graph.entities;
26539
+ if (entities.length > this.maxSnapshotSize) {
26540
+ entities = [...entities].sort((a, b) => (b.importance ?? 0) - (a.importance ?? 0)).slice(0, this.maxSnapshotSize);
26541
+ }
26542
+ const snapshotEntities = entities.map((e) => ({
26543
+ name: e.name,
26544
+ entityType: e.entityType,
26545
+ importance: e.importance,
26546
+ confidence: e.confidence,
26547
+ observationCount: e.observations.length,
26548
+ tags: [...e.tags ?? []],
26549
+ lastModified: e.lastModified
26550
+ }));
26551
+ return new WorldStateSnapshot(snapshotEntities);
26552
+ }
26553
+ /**
26554
+ * Validate a candidate observation against the named entity's current
26555
+ * state. Delegates to `MemoryValidator.validateConsistency` when one
26556
+ * was wired at construction; returns a deferred result with a `null`
26557
+ * `issues` array when no validator is available — callers should treat
26558
+ * `valid: undefined` as "not checked" rather than "passed".
26559
+ */
26560
+ async validateFact(observation, entityName) {
26561
+ if (!this.memoryValidator) return null;
26562
+ const entity = await this.entityManager.getEntity(entityName);
26563
+ if (!entity) return null;
26564
+ return this.memoryValidator.validateConsistency(observation, entity);
26565
+ }
26566
+ /**
26567
+ * Predict downstream effects of an action by walking the causal
26568
+ * subgraph from `actionEntity` to each candidate effect. Returns
26569
+ * empty when no causal reasoner was wired or no chain reaches any
26570
+ * candidate.
26571
+ */
26572
+ async predictOutcome(actionEntity, candidateEffects) {
26573
+ if (!this.causalReasoner) return [];
26574
+ return this.causalReasoner.findEffects(actionEntity, candidateEffects);
26575
+ }
26576
+ /**
26577
+ * Pure: diff two snapshots. Direct passthrough to
26578
+ * `WorldStateSnapshot.diffTo` — exposed here so callers can use the
26579
+ * world-model facade for both snapshotting and change detection.
26580
+ */
26581
+ detectStateChange(before, after) {
26582
+ return before.diffTo(after);
26583
+ }
26584
+ };
26585
+ }
26586
+ });
26587
+
26588
+ // src/agent/retrieval/QueryRewriter.ts
26589
+ function tokenize7(s) {
26590
+ return s.toLowerCase().split(/[^a-z0-9]+/g).filter((t) => t.length >= 3 && !STOPWORDS3.has(t));
26591
+ }
26592
+ var STOPWORDS3, QueryRewriter;
26593
+ var init_QueryRewriter = __esm({
26594
+ "src/agent/retrieval/QueryRewriter.ts"() {
26595
+ "use strict";
26596
+ init_esm_shims();
26597
+ STOPWORDS3 = /* @__PURE__ */ new Set([
26598
+ "the",
26599
+ "a",
26600
+ "an",
26601
+ "of",
26602
+ "and",
26603
+ "or",
26604
+ "but",
26605
+ "is",
26606
+ "are",
26607
+ "was",
26608
+ "were",
26609
+ "be",
26610
+ "been",
26611
+ "being",
26612
+ "have",
26613
+ "has",
26614
+ "had",
26615
+ "do",
26616
+ "does",
26617
+ "did",
26618
+ "will",
26619
+ "would",
26620
+ "could",
26621
+ "should",
26622
+ "may",
26623
+ "might",
26624
+ "must",
26625
+ "shall",
26626
+ "can",
26627
+ "to",
26628
+ "for",
26629
+ "with",
26630
+ "on",
26631
+ "in",
26632
+ "at",
26633
+ "by",
26634
+ "from",
26635
+ "up",
26636
+ "about",
26637
+ "as",
26638
+ "this",
26639
+ "that",
26640
+ "these",
26641
+ "those",
26642
+ "i",
26643
+ "you",
26644
+ "he",
26645
+ "she",
26646
+ "it",
26647
+ "we",
26648
+ "they",
26649
+ "his",
26650
+ "her",
26651
+ "its",
26652
+ "our",
26653
+ "their",
26654
+ "me",
26655
+ "him",
26656
+ "them",
26657
+ "us",
26658
+ "my",
26659
+ "your"
26660
+ ]);
26661
+ QueryRewriter = class {
26662
+ /**
26663
+ * Expand `query` with the top-`expansionLimit` co-occurring tokens
26664
+ * from `snippets`. Tokens already present in the query (case-
26665
+ * insensitive) and stopwords are excluded.
26666
+ */
26667
+ rewrite(query, snippets, expansionLimit = 3) {
26668
+ const queryTokens = new Set(tokenize7(query));
26669
+ const counts = /* @__PURE__ */ new Map();
26670
+ for (const s of snippets) {
26671
+ const seen = /* @__PURE__ */ new Set();
26672
+ for (const t of tokenize7(s)) {
26673
+ if (queryTokens.has(t)) continue;
26674
+ if (seen.has(t)) continue;
26675
+ seen.add(t);
26676
+ counts.set(t, (counts.get(t) ?? 0) + 1);
26677
+ }
26678
+ }
26679
+ const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, expansionLimit).map(([t]) => t);
26680
+ if (top.length === 0) {
26681
+ return { query, expansionTokens: [] };
26682
+ }
26683
+ return {
26684
+ query: `${query} ${top.join(" ")}`,
26685
+ expansionTokens: top
26686
+ };
26687
+ }
26688
+ };
26689
+ }
26690
+ });
26691
+
26692
+ // src/agent/retrieval/ActiveRetrievalController.ts
26693
+ var ActiveRetrievalController;
26694
+ var init_ActiveRetrievalController = __esm({
26695
+ "src/agent/retrieval/ActiveRetrievalController.ts"() {
26696
+ "use strict";
26697
+ init_esm_shims();
26698
+ init_QueryRewriter();
26699
+ ActiveRetrievalController = class {
26700
+ constructor(rankedSearch, config = {}) {
26701
+ this.rankedSearch = rankedSearch;
26702
+ this.maxRounds = config.maxRounds ?? 3;
26703
+ this.minCoverage = config.minCoverage ?? 0.6;
26704
+ this.resultsPerRound = config.resultsPerRound ?? 10;
26705
+ this.costThreshold = config.costThreshold ?? 1e3;
26706
+ this.expansionLimit = config.expansionLimit ?? 3;
26707
+ }
26708
+ rewriter = new QueryRewriter();
26709
+ maxRounds;
26710
+ minCoverage;
26711
+ resultsPerRound;
26712
+ costThreshold;
26713
+ expansionLimit;
26714
+ /**
26715
+ * Decide whether retrieval is worth the cost. Currently a simple
26716
+ * heuristic: tokens(query) × resultsPerRound × maxRounds × constant.
26717
+ * Returns `retrieve: false` when estimated cost > budget OR query is
26718
+ * empty.
26719
+ */
26720
+ shouldRetrieve(context) {
26721
+ const tokens = context.query.trim().split(/\s+/).filter(Boolean);
26722
+ if (tokens.length === 0) {
26723
+ return { retrieve: false, estimatedCost: 0, reason: "Empty query" };
26724
+ }
26725
+ const estimatedCost = tokens.length * 5 + this.resultsPerRound * this.maxRounds * 50;
26726
+ const budget = context.budgetTokens ?? this.costThreshold;
26727
+ if (estimatedCost > budget) {
26728
+ return {
26729
+ retrieve: false,
26730
+ estimatedCost,
26731
+ reason: `Estimated cost ${estimatedCost} exceeds budget ${budget}`
26732
+ };
26733
+ }
26734
+ return {
26735
+ retrieve: true,
26736
+ estimatedCost,
26737
+ reason: "Cost within budget"
26738
+ };
26739
+ }
26740
+ /**
26741
+ * Run up to `maxRounds` of (search → score coverage → rewrite). Stops
26742
+ * early when coverage hits `minCoverage` or no expansion tokens are
26743
+ * available. Returns the highest-coverage round's results plus the
26744
+ * full per-round trace.
26745
+ */
26746
+ async adaptiveRetrieve(context) {
26747
+ const rounds = [];
26748
+ let currentQuery = context.query;
26749
+ let bestRound = null;
26750
+ for (let i = 0; i < this.maxRounds; i++) {
26751
+ const results = await this.rankedSearch.searchNodesRanked(
26752
+ currentQuery,
26753
+ void 0,
26754
+ void 0,
26755
+ void 0,
26756
+ this.resultsPerRound
26757
+ );
26758
+ const coverage = this.estimateCoverage(results);
26759
+ const round = {
26760
+ query: currentQuery,
26761
+ results,
26762
+ coverage,
26763
+ expansionTokens: i === 0 ? [] : rounds[i - 1].expansionTokens ?? []
26764
+ };
26765
+ rounds.push(round);
26766
+ if (!bestRound || coverage > bestRound.coverage) bestRound = round;
26767
+ if (coverage >= this.minCoverage) break;
26768
+ if (results.length === 0) break;
26769
+ const snippets = results.flatMap((r) => r.entity.observations).slice(0, 20);
26770
+ const rewrite = this.rewriter.rewrite(currentQuery, snippets, this.expansionLimit);
26771
+ if (rewrite.expansionTokens.length === 0) break;
26772
+ currentQuery = rewrite.query;
26773
+ rounds[rounds.length - 1].expansionTokens = rewrite.expansionTokens;
26774
+ }
26775
+ return {
26776
+ bestResults: bestRound?.results ?? [],
26777
+ bestCoverage: bestRound?.coverage ?? 0,
26778
+ rounds
26779
+ };
26780
+ }
26781
+ /**
26782
+ * Quick coverage estimate: average of the top-3 results' scores,
26783
+ * clamped to [0, 1]. Empty results → 0. Score normalization assumes
26784
+ * `RankedSearch` returns BM25-ish positive numbers; we cap at 1.0 to
26785
+ * keep the threshold comparison meaningful.
26786
+ */
26787
+ estimateCoverage(results) {
26788
+ if (results.length === 0) return 0;
26789
+ const top = results.slice(0, 3);
26790
+ const avg = top.reduce((acc, r) => acc + (r.score ?? 0), 0) / top.length;
26791
+ return Math.min(1, Math.max(0, avg));
26792
+ }
26793
+ };
26794
+ }
26795
+ });
26796
+
26797
+ // src/core/ManagerContext.ts
26798
+ import path5 from "path";
26799
+ var ManagerContext;
26800
+ var init_ManagerContext = __esm({
26801
+ "src/core/ManagerContext.ts"() {
26802
+ "use strict";
26803
+ init_esm_shims();
26804
+ init_StorageFactory();
26805
+ init_EntityManager();
26806
+ init_RelationManager();
26807
+ init_ObservationManager();
26808
+ init_HierarchyManager();
26809
+ init_GraphTraversal();
26810
+ init_SearchManager();
26811
+ init_RankedSearch();
26812
+ init_LLMQueryPlanner();
26813
+ init_LLMSearchExecutor();
26814
+ init_search();
26815
+ init_IOManager();
26816
+ init_TagManager();
26817
+ init_AnalyticsManager();
26818
+ init_CompressionManager();
26819
+ init_ArchiveManager();
26820
+ init_AutoLinker();
26821
+ init_FactExtractor();
26822
+ init_TransitionLedger();
26823
+ init_AccessTracker();
26824
+ init_DecayEngine();
26825
+ init_DecayScheduler();
26826
+ init_ConsolidationScheduler();
26827
+ init_SalienceEngine();
26828
+ init_ContextWindowManager();
26829
+ init_MemoryFormatter();
26830
+ init_AgentMemoryManager();
26831
+ init_ArtifactManager();
26832
+ init_DreamEngine();
26833
+ init_RefIndex();
26834
+ init_ObserverPipeline();
26835
+ init_constants();
26836
+ init_utils();
26837
+ init_ContradictionDetector();
26838
+ init_SemanticForget();
26839
+ init_MemoryEngine();
26840
+ init_ImportanceScorer();
26841
+ init_InMemoryBackend();
26842
+ init_SQLiteBackend();
26843
+ init_MemoryValidator();
26844
+ init_TrajectoryCompressor();
26845
+ init_ExperienceExtractor();
26846
+ init_PatternDetector();
26847
+ init_ProcedureManager();
26848
+ init_CausalReasoner();
26849
+ init_RbacMiddleware();
26850
+ init_RoleAssignmentStore();
26851
+ init_WorldModelManager();
26852
+ init_ActiveRetrievalController();
26853
+ ManagerContext = class {
26854
+ // Type as GraphStorage for manager compatibility; actual instance may be SQLiteStorage
26855
+ // which implements the same interface via duck typing
26856
+ storage;
26857
+ defaultProjectId;
26858
+ savedSearchesFilePath;
26859
+ tagAliasesFilePath;
26860
+ refIndexFilePath;
26861
+ _observerPipeline;
26862
+ // ==================== LAZY-INITIALIZED CORE MANAGERS ====================
26863
+ _entityManager;
26864
+ _relationManager;
26865
+ _observationManager;
26866
+ _hierarchyManager;
26867
+ _graphTraversal;
26868
+ _searchManager;
26869
+ _rankedSearch;
26870
+ _ioManager;
26871
+ _tagManager;
26872
+ _analyticsManager;
26873
+ _compressionManager;
26874
+ _archiveManager;
26875
+ _autoLinker;
26876
+ _factExtractor;
26877
+ _transitionLedger;
26878
+ _semanticSearch;
26879
+ _memoryEngine;
26880
+ _memoryBackend;
26881
+ _memoryValidator;
26882
+ _trajectoryCompressor;
26883
+ _experienceExtractor;
26884
+ _patternDetector;
26885
+ _procedureManager;
26886
+ _causalReasoner;
26887
+ _rbacMiddleware;
26888
+ _roleAssignmentStore;
26889
+ _worldModelManager;
26890
+ _activeRetrieval;
26891
+ _accessTracker;
26892
+ _decayEngine;
26893
+ _decayScheduler;
26894
+ _salienceEngine;
26895
+ _contextWindowManager;
26896
+ _memoryFormatter;
26897
+ _agentMemory;
26898
+ _refIndex;
26899
+ _artifactManager;
26900
+ _consolidationScheduler;
26901
+ _dreamEngine;
26902
+ _llmQueryPlanner;
26903
+ _llmSearchExecutor;
26904
+ _semanticForget;
26905
+ constructor(pathOrOptions) {
26906
+ const opts = typeof pathOrOptions === "string" ? { storagePath: pathOrOptions } : pathOrOptions;
26907
+ this.defaultProjectId = opts.defaultProjectId;
26908
+ const validatedPath = validateFilePath(opts.storagePath, void 0, false);
26909
+ const dir = path5.dirname(validatedPath);
26910
+ const basename2 = path5.basename(validatedPath, path5.extname(validatedPath));
26911
+ this.savedSearchesFilePath = path5.join(dir, `${basename2}-saved-searches.jsonl`);
26912
+ this.tagAliasesFilePath = path5.join(dir, `${basename2}-tag-aliases.jsonl`);
26913
+ this.refIndexFilePath = path5.join(dir, `${basename2}-ref-index.jsonl`);
26914
+ this.storage = createStorageFromPath(validatedPath);
26915
+ if (opts.enableContradictionDetection) {
26916
+ this.initContradictionDetection(opts.contradictionThreshold);
26917
+ }
26918
+ this.observationManager.setMemoryValidator(() => this.memoryValidator);
26919
+ }
26920
+ /**
26921
+ * Wire ContradictionDetector to ObservationManager if a semantic search
26922
+ * embedding provider is available. Silently degrades when none is configured.
26923
+ * @internal
26924
+ */
26925
+ initContradictionDetection(threshold) {
26926
+ try {
26927
+ const ss = this.semanticSearch;
26928
+ if (!ss) {
26929
+ console.warn(
26930
+ "[ManagerContext] Contradiction detection requested but no embedding provider is configured. Set MEMORY_EMBEDDING_PROVIDER to enable it."
26931
+ );
26932
+ return;
26933
+ }
26934
+ const detector = new ContradictionDetector(ss, threshold ?? 0.85);
26935
+ this.observationManager.setContradictionDetector(detector, this.entityManager);
26936
+ } catch (err) {
26937
+ console.warn(
26938
+ "[ManagerContext] Could not initialise contradiction detection:",
26939
+ err instanceof Error ? err.message : String(err)
26940
+ );
26941
+ }
26942
+ }
26943
+ // ==================== LAZY ACCESSORS (agent memory + semantic) ====================
26944
+ /** EntityManager - Entity CRUD and tag operations */
26945
+ get entityManager() {
26946
+ return this._entityManager ??= new EntityManager(
26947
+ this.storage,
26948
+ { defaultProjectId: this.defaultProjectId }
26949
+ );
26950
+ }
26951
+ /** RelationManager - Relation CRUD */
26952
+ get relationManager() {
26953
+ return this._relationManager ??= new RelationManager(this.storage);
26954
+ }
26955
+ /** ObservationManager - Observation CRUD */
26956
+ get observationManager() {
26957
+ return this._observationManager ??= new ObservationManager(this.storage);
26958
+ }
26959
+ /** HierarchyManager - Entity hierarchy operations */
26960
+ get hierarchyManager() {
26961
+ return this._hierarchyManager ??= new HierarchyManager(this.storage);
26962
+ }
26963
+ /** GraphTraversal - Phase 4 Sprint 6-8: Graph traversal algorithms */
26964
+ get graphTraversal() {
26965
+ return this._graphTraversal ??= new GraphTraversal(this.storage);
26966
+ }
26967
+ /** SearchManager - All search operations */
26968
+ get searchManager() {
26969
+ return this._searchManager ??= new SearchManager(this.storage, this.savedSearchesFilePath);
23856
26970
  }
23857
26971
  /** RankedSearch - TF-IDF/BM25 ranked search */
23858
26972
  get rankedSearch() {
@@ -23918,6 +27032,203 @@ var init_ManagerContext = __esm({
23918
27032
  }
23919
27033
  return this._semanticSearch;
23920
27034
  }
27035
+ /**
27036
+ * MemoryEngine — turn-aware conversation memory facade composing over
27037
+ * EpisodicMemoryManager + WorkingMemoryManager + ImportanceScorer.
27038
+ * Lazy: instantiated on first access. Reads MEMORY_ENGINE_* env vars
27039
+ * for dedup thresholds, scan window, and scorer weights.
27040
+ */
27041
+ get memoryEngine() {
27042
+ if (!this._memoryEngine) {
27043
+ const agent = this.agentMemory();
27044
+ const importanceScorer = new ImportanceScorer({
27045
+ lengthWeight: this.getEnvNumber("MEMORY_ENGINE_LENGTH_WEIGHT", 0.3),
27046
+ keywordWeight: this.getEnvNumber("MEMORY_ENGINE_KEYWORD_WEIGHT", 0.4),
27047
+ overlapWeight: this.getEnvNumber("MEMORY_ENGINE_OVERLAP_WEIGHT", 0.3)
27048
+ });
27049
+ const semanticSearch = this.semanticSearch ?? null;
27050
+ const embeddingService = semanticSearch?.getEmbeddingService() ?? null;
27051
+ this._memoryEngine = new MemoryEngine(
27052
+ this.storage,
27053
+ this.entityManager,
27054
+ agent.episodicMemory,
27055
+ agent.workingMemory,
27056
+ importanceScorer,
27057
+ semanticSearch,
27058
+ embeddingService,
27059
+ {
27060
+ jaccardThreshold: this.getEnvNumber("MEMORY_ENGINE_JACCARD_THRESHOLD", 0.72),
27061
+ prefixOverlapThreshold: this.getEnvNumber("MEMORY_ENGINE_PREFIX_OVERLAP", 0.5),
27062
+ dedupScanWindow: Math.trunc(
27063
+ this.getEnvNumber("MEMORY_ENGINE_DEDUP_SCAN_WINDOW", 200)
27064
+ ),
27065
+ maxTurnsPerSession: Math.trunc(
27066
+ this.getEnvNumber("MEMORY_ENGINE_MAX_TURNS_PER_SESSION", 1e3)
27067
+ ),
27068
+ semanticDedupEnabled: this.getEnvBool("MEMORY_ENGINE_SEMANTIC_DEDUP", false),
27069
+ semanticThreshold: this.getEnvNumber("MEMORY_ENGINE_SEMANTIC_THRESHOLD", 0.92),
27070
+ recentTurnsForImportance: Math.trunc(
27071
+ this.getEnvNumber("MEMORY_ENGINE_RECENT_TURNS", 10)
27072
+ )
27073
+ }
27074
+ );
27075
+ }
27076
+ return this._memoryEngine;
27077
+ }
27078
+ /**
27079
+ * IMemoryBackend (PRD MEM-04) — agent-memory-flavored backend.
27080
+ *
27081
+ * Selection by `MEMORY_BACKEND` env var:
27082
+ * - `sqlite` (default when storage is SQLite OR var unset on JSONL)
27083
+ * - `in-memory` (ephemeral; no persistence)
27084
+ * - future: `postgres`, `vector` (Phase γ)
27085
+ *
27086
+ * Both adapters wrap `ctx.memoryEngine` + `ctx.decayEngine` so they
27087
+ * inherit the four-tier dedup chain and PRD effective-importance
27088
+ * scoring respectively. Lazy-initialized; cached.
27089
+ */
27090
+ get memoryBackend() {
27091
+ if (!this._memoryBackend) {
27092
+ const choice = (process.env.MEMORY_BACKEND ?? "sqlite").toLowerCase();
27093
+ switch (choice) {
27094
+ case "in-memory":
27095
+ case "inmemory":
27096
+ case "memory":
27097
+ this._memoryBackend = new InMemoryBackend(this.decayEngine);
27098
+ break;
27099
+ case "sqlite":
27100
+ default:
27101
+ this._memoryBackend = new SQLiteBackend(this.memoryEngine, this.decayEngine);
27102
+ break;
27103
+ }
27104
+ }
27105
+ return this._memoryBackend;
27106
+ }
27107
+ /**
27108
+ * MemoryValidator (ROADMAP §3B.1, Phase δ.1) — reflection-stage
27109
+ * service that prevents hallucinations and logical errors from
27110
+ * contaminating memory through self-critique before storage.
27111
+ * Wraps `ContradictionDetector`. Lazy-initialized.
27112
+ *
27113
+ * Construction needs `ContradictionDetector(SemanticSearch, threshold)`.
27114
+ * If no semantic-search backend is configured, a no-op detector is
27115
+ * synthesized so MemoryValidator's other methods still work — the
27116
+ * detection method just returns no contradictions.
27117
+ */
27118
+ get memoryValidator() {
27119
+ if (!this._memoryValidator) {
27120
+ const ss = this.semanticSearch;
27121
+ const detector = ss ? new ContradictionDetector(ss, 0.85) : new ContradictionDetector(
27122
+ { calculateSimilarity: async () => 0 },
27123
+ 0.85
27124
+ );
27125
+ this._memoryValidator = new MemoryValidator(detector);
27126
+ }
27127
+ return this._memoryValidator;
27128
+ }
27129
+ /**
27130
+ * TrajectoryCompressor (ROADMAP §3B.2, Phase δ.2) — reflection-stage
27131
+ * service that distills verbose interaction histories into compact,
27132
+ * reusable representations. Wraps `compressForContext`. Lazy.
27133
+ */
27134
+ get trajectoryCompressor() {
27135
+ if (!this._trajectoryCompressor) {
27136
+ this._trajectoryCompressor = new TrajectoryCompressor(this.contextWindowManager);
27137
+ }
27138
+ return this._trajectoryCompressor;
27139
+ }
27140
+ /**
27141
+ * ExperienceExtractor (ROADMAP §3B.3, Phase δ.3) — experience-stage
27142
+ * service that abstracts universal patterns from trajectory clusters
27143
+ * for zero-shot transfer. Wraps `PatternDetector`. Lazy.
27144
+ */
27145
+ get experienceExtractor() {
27146
+ if (!this._experienceExtractor) {
27147
+ this._experienceExtractor = new ExperienceExtractor(this.patternDetector);
27148
+ }
27149
+ return this._experienceExtractor;
27150
+ }
27151
+ /** Lazy `PatternDetector` instance — backs `experienceExtractor`
27152
+ * but also exposed directly for callers that want pattern detection
27153
+ * without the full Experience-stage wrapper. */
27154
+ get patternDetector() {
27155
+ if (!this._patternDetector) {
27156
+ this._patternDetector = new PatternDetector();
27157
+ }
27158
+ return this._patternDetector;
27159
+ }
27160
+ /**
27161
+ * `ProcedureManager` (3B.4) — first-class executable procedures.
27162
+ * Lazy. Composes `EntityManager` (persists procedures as
27163
+ * `entityType: 'procedure'`).
27164
+ */
27165
+ get procedureManager() {
27166
+ if (!this._procedureManager) {
27167
+ this._procedureManager = new ProcedureManager(this.entityManager);
27168
+ }
27169
+ return this._procedureManager;
27170
+ }
27171
+ /**
27172
+ * `CausalReasoner` (3B.6) — symbolic forward / backward / counter-
27173
+ * factual inference over the causal subgraph. Wraps `GraphTraversal`.
27174
+ * Lazy.
27175
+ */
27176
+ get causalReasoner() {
27177
+ if (!this._causalReasoner) {
27178
+ this._causalReasoner = new CausalReasoner(this.graphTraversal);
27179
+ }
27180
+ return this._causalReasoner;
27181
+ }
27182
+ /**
27183
+ * `RoleAssignmentStore` (η.6.1) — registry of role grants per agent.
27184
+ * Lazy. In-memory only by default; configure persistence via the
27185
+ * direct constructor if a JSONL sidecar is wanted.
27186
+ */
27187
+ get roleAssignmentStore() {
27188
+ if (!this._roleAssignmentStore) {
27189
+ this._roleAssignmentStore = new RoleAssignmentStore();
27190
+ }
27191
+ return this._roleAssignmentStore;
27192
+ }
27193
+ /**
27194
+ * `RbacMiddleware` (η.6.1) — `RbacPolicy` impl. Backed by the lazy
27195
+ * `roleAssignmentStore`. Lazy.
27196
+ */
27197
+ get rbacMiddleware() {
27198
+ if (!this._rbacMiddleware) {
27199
+ this._rbacMiddleware = new RbacMiddleware(this.roleAssignmentStore);
27200
+ }
27201
+ return this._rbacMiddleware;
27202
+ }
27203
+ /**
27204
+ * `WorldModelManager` (3B.7) — orchestrator that composes
27205
+ * `causalReasoner`, `memoryValidator`, and `entityManager` into a
27206
+ * single facade for `getCurrentState` / `validateFact` /
27207
+ * `predictOutcome` / `detectStateChange`. Lazy. Causal and validator
27208
+ * dependencies are passed through; methods that need them gracefully
27209
+ * return null/empty when they're not configured.
27210
+ */
27211
+ get worldModelManager() {
27212
+ if (!this._worldModelManager) {
27213
+ this._worldModelManager = new WorldModelManager(
27214
+ this.entityManager,
27215
+ this.causalReasoner,
27216
+ this.memoryValidator
27217
+ );
27218
+ }
27219
+ return this._worldModelManager;
27220
+ }
27221
+ /**
27222
+ * `ActiveRetrievalController` (3B.5) — adaptive query-rewriting loop
27223
+ * over `RankedSearch`. Lazy. Pure symbolic expansion (no LLM); for
27224
+ * LLM-driven decomposition use `ctx.llmQueryPlanner`.
27225
+ */
27226
+ get activeRetrieval() {
27227
+ if (!this._activeRetrieval) {
27228
+ this._activeRetrieval = new ActiveRetrievalController(this.rankedSearch);
27229
+ }
27230
+ return this._activeRetrieval;
27231
+ }
23921
27232
  /**
23922
27233
  * TransitionLedger - Append-only audit trail for state changes.
23923
27234
  * Returns null if not enabled via MEMORY_TRANSITION_LEDGER env var.
@@ -23980,11 +27291,25 @@ var init_ManagerContext = __esm({
23980
27291
  halfLifeHours: this.getEnvNumber("MEMORY_DECAY_HALF_LIFE_HOURS", 168),
23981
27292
  minImportance: this.getEnvNumber("MEMORY_DECAY_MIN_IMPORTANCE", 0.1),
23982
27293
  importanceModulation: this.getEnvBool("MEMORY_DECAY_IMPORTANCE_MOD", true),
23983
- accessModulation: this.getEnvBool("MEMORY_DECAY_ACCESS_MOD", true)
27294
+ accessModulation: this.getEnvBool("MEMORY_DECAY_ACCESS_MOD", true),
27295
+ // PRD MEM-01 (v1.12.0). decayRate is auto-derived from halfLifeHours
27296
+ // when env-var unset (NaN check avoids overriding the auto-derive).
27297
+ decayRate: this.envNumberOrUndefined("MEMORY_PRD_DECAY_RATE"),
27298
+ freshnessCoefficient: this.getEnvNumber("MEMORY_PRD_FRESHNESS_COEFFICIENT", 0.01),
27299
+ relevanceWeight: this.getEnvNumber("MEMORY_PRD_RELEVANCE_WEIGHT", 0.35),
27300
+ minImportanceThreshold: this.getEnvNumber("MEMORY_PRD_MIN_IMPORTANCE_THRESHOLD", 0.1)
23984
27301
  });
23985
27302
  }
23986
27303
  return this._decayEngine;
23987
27304
  }
27305
+ /** Returns env-var as number, or undefined when unset (lets the
27306
+ * default-derive logic in DecayEngine kick in for `decayRate`). */
27307
+ envNumberOrUndefined(name) {
27308
+ const raw = process.env[name];
27309
+ if (raw === void 0 || raw === "") return void 0;
27310
+ const n = Number(raw);
27311
+ return Number.isFinite(n) ? n : void 0;
27312
+ }
23988
27313
  /**
23989
27314
  * DecayScheduler - Scheduled decay and forget operations.
23990
27315
  * Returns undefined if auto-decay is not enabled (MEMORY_AUTO_DECAY).
@@ -24145,6 +27470,10 @@ var init_ManagerContext = __esm({
24145
27470
  agentMemory(config) {
24146
27471
  if (!this._agentMemory || config) {
24147
27472
  this._agentMemory = new AgentMemoryManager(this.storage, config);
27473
+ this._memoryEngine = void 0;
27474
+ this._memoryBackend = void 0;
27475
+ this._consolidationScheduler = void 0;
27476
+ this._dreamEngine = void 0;
24148
27477
  }
24149
27478
  return this._agentMemory;
24150
27479
  }
@@ -24427,7 +27756,7 @@ Path (${pathResult.length} hops): ${pathResult.path.join(" -> ")}`);
24427
27756
  break;
24428
27757
  }
24429
27758
  case "export": {
24430
- const validFormats = ["json", "csv", "graphml", "gexf", "dot", "markdown", "mermaid"];
27759
+ const validFormats = ["json", "csv", "graphml", "gexf", "dot", "markdown", "mermaid", "turtle", "rdf-xml", "json-ld"];
24431
27760
  const fmt = args[0] || "json";
24432
27761
  if (!validFormats.includes(fmt)) {
24433
27762
  console.log(chalk2.yellow(`Invalid format: ${fmt}. Use: ${validFormats.join(", ")}`));
@@ -24501,14 +27830,11 @@ var defaultOptions = {
24501
27830
  verbose: false
24502
27831
  };
24503
27832
  function parseGlobalOptions(opts) {
24504
- const format = opts.format;
24505
- if (format && !["json", "table", "csv"].includes(format)) {
24506
- console.error(`Invalid format: ${format}. Use json, table, or csv.`);
24507
- process.exit(1);
24508
- }
27833
+ const rawFormat = opts.outputFormat;
27834
+ const format = rawFormat && ["json", "table", "csv"].includes(rawFormat) ? rawFormat : defaultOptions.format;
24509
27835
  return {
24510
27836
  storage: opts.storage || defaultOptions.storage,
24511
- format: format || defaultOptions.format,
27837
+ format,
24512
27838
  quiet: Boolean(opts.quiet),
24513
27839
  verbose: Boolean(opts.verbose)
24514
27840
  };
@@ -25365,8 +28691,9 @@ init_esm_shims();
25365
28691
  import { readFileSync as readFileSync2, writeFileSync } from "fs";
25366
28692
  import { resolve as resolve2 } from "path";
25367
28693
  import { Option } from "commander";
28694
+ init_entityUtils();
25368
28695
  var IMPORT_FORMATS = ["json", "csv", "graphml"];
25369
- var EXPORT_FORMATS = ["json", "csv", "graphml", "gexf", "dot", "markdown", "mermaid"];
28696
+ var EXPORT_FORMATS = ["json", "csv", "graphml", "gexf", "dot", "markdown", "mermaid", "turtle", "rdf-xml", "json-ld"];
25370
28697
  function registerIOCommands(program2) {
25371
28698
  program2.command("import <file>").description("Import data from file").addOption(
25372
28699
  new Option("-f, --format <format>", "File format").choices([...IMPORT_FORMATS]).default("json")
@@ -25375,7 +28702,7 @@ function registerIOCommands(program2) {
25375
28702
  const logger2 = createLogger(options);
25376
28703
  const ctx = createContext(options);
25377
28704
  try {
25378
- const resolvedPath = resolve2(file);
28705
+ const resolvedPath = validateFilePath(resolve2(file), void 0, false);
25379
28706
  const data = readFileSync2(resolvedPath, "utf-8");
25380
28707
  const result = await ctx.ioManager.importGraph(
25381
28708
  opts.format,
@@ -25397,7 +28724,7 @@ function registerIOCommands(program2) {
25397
28724
  const logger2 = createLogger(options);
25398
28725
  const ctx = createContext(options);
25399
28726
  try {
25400
- const resolvedPath = resolve2(file);
28727
+ const resolvedPath = validateFilePath(resolve2(file), void 0, false);
25401
28728
  const graph = await ctx.storage.loadGraph();
25402
28729
  const data = ctx.ioManager.exportGraph(
25403
28730
  graph,
@@ -25568,7 +28895,7 @@ function getVersion() {
25568
28895
  }
25569
28896
  var program = new Command2();
25570
28897
  program.name("memory").description("MemoryJS - Knowledge Graph CLI").version(getVersion(), "-v, --version", "Output the current version");
25571
- program.option("-s, --storage <path>", "Path to storage file", "./memory.jsonl").option("-f, --format <type>", "Output format (json|table|csv)", "json").option("-q, --quiet", "Suppress non-essential output").option("--verbose", "Enable verbose/debug output");
28898
+ program.option("-s, --storage <path>", "Path to storage file", "./memory.jsonl").option("--output-format <type>", "Console output format (json|table|csv)", "json").option("-q, --quiet", "Suppress non-essential output").option("--verbose", "Enable verbose/debug output");
25572
28899
  registerCommands(program);
25573
28900
  program.parse();
25574
28901
  //# sourceMappingURL=index.js.map