@gamaze/hicortex 0.7.0 → 0.7.1

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.
@@ -43,6 +43,7 @@ exports.parseJsonLenient = parseJsonLenient;
43
43
  exports.runConsolidation = runConsolidation;
44
44
  exports.msUntilHour = msUntilHour;
45
45
  exports.scheduleConsolidation = scheduleConsolidation;
46
+ const types_js_1 = require("./types.js");
46
47
  const retrieval_js_1 = require("./retrieval.js");
47
48
  const storage = __importStar(require("./storage.js"));
48
49
  const prompts_js_1 = require("./prompts.js");
@@ -487,12 +488,17 @@ async function stageDomainCuration(db, llm, budget, dryRun, stateDir) {
487
488
  console.log(`[hicortex] Domain curation: ${domains.length} domains from ${projectRows.length} projects`);
488
489
  return { curated: true, domains: domains.length };
489
490
  }
490
- // ---------------------------------------------------------------------------
491
- // Stage 3: Link Discovery (vector similarity auto-link)
492
- // ---------------------------------------------------------------------------
493
- async function stageLinks(db, memories, embedFn, dryRun) {
491
+ /** Batch size for LLM edge classification calls. */
492
+ const EDGE_CLASSIFICATION_BATCH_SIZE = 8;
493
+ /** Valid relationship type set for fast lookup. */
494
+ const VALID_REL_SET = new Set(types_js_1.VALID_RELATIONSHIP_TYPES);
495
+ async function stageLinks(db, memories, embedFn, dryRun, llm, budget) {
494
496
  let autoLinked = 0;
497
+ let llmClassified = 0;
498
+ let heuristicFallback = 0;
495
499
  let failed = 0;
500
+ // Phase A: Discovery — collect candidates via vector similarity
501
+ const candidates = [];
496
502
  for (const mem of memories) {
497
503
  try {
498
504
  const embedding = await embedFn(mem.content);
@@ -500,27 +506,78 @@ async function stageLinks(db, memories, embedFn, dryRun) {
500
506
  for (const neighbor of neighbors) {
501
507
  const similarity = 1.0 - neighbor.distance;
502
508
  if (similarity > CONSOLIDATE_LINK_THRESHOLD) {
503
- const relationship = classifyRelationship(mem, neighbor, similarity);
504
- if (!dryRun) {
505
- try {
506
- storage.addLink(db, mem.id, neighbor.id, relationship, similarity);
507
- autoLinked++;
509
+ const heuristicType = classifyRelationship(mem, neighbor, similarity);
510
+ candidates.push({ source: mem, target: neighbor, similarity, heuristicType });
511
+ }
512
+ }
513
+ }
514
+ catch {
515
+ failed++;
516
+ }
517
+ }
518
+ if (candidates.length === 0) {
519
+ return { auto_linked: 0, llm_classified: 0, heuristic_fallback: 0, failed };
520
+ }
521
+ // Phase B: LLM batch classification
522
+ // Build batches and classify with LLM where budget allows
523
+ const classifiedTypes = new Array(candidates.length);
524
+ for (let i = 0; i < candidates.length; i += EDGE_CLASSIFICATION_BATCH_SIZE) {
525
+ const batch = candidates.slice(i, i + EDGE_CLASSIFICATION_BATCH_SIZE);
526
+ // Attempt LLM classification if budget allows
527
+ if (budget.use("edge_classification")) {
528
+ try {
529
+ const pairsBlock = batch.map((c, idx) => {
530
+ const srcContent = c.source.content.slice(0, 200);
531
+ const tgtContent = c.target.content.slice(0, 200);
532
+ return `[${idx}] SOURCE: ${c.source.memory_type} | ${c.source.project ?? "global"} | ${srcContent}\n TARGET: ${c.target.memory_type} | ${c.target.project ?? "global"} | ${tgtContent}\n similarity: ${c.similarity.toFixed(2)}`;
533
+ }).join("\n\n");
534
+ const prompt = (0, prompts_js_1.edgeClassification)(pairsBlock);
535
+ const raw = await llm.completeFast(prompt, 512);
536
+ const parsed = parseJsonLenient(raw, []);
537
+ if (Array.isArray(parsed) && parsed.length > 0) {
538
+ for (let j = 0; j < batch.length; j++) {
539
+ const llmType = parsed[j];
540
+ if (typeof llmType === "string" && VALID_REL_SET.has(llmType)) {
541
+ classifiedTypes[i + j] = llmType;
542
+ llmClassified++;
508
543
  }
509
- catch {
510
- failed++;
544
+ else {
545
+ // Invalid type from LLM — fall back to heuristic
546
+ classifiedTypes[i + j] = batch[j].heuristicType;
547
+ heuristicFallback++;
511
548
  }
512
549
  }
513
- else {
514
- autoLinked++;
515
- }
550
+ continue;
516
551
  }
517
552
  }
553
+ catch {
554
+ // LLM call failed — fall through to heuristic for this batch
555
+ }
518
556
  }
519
- catch {
520
- failed++;
557
+ // Budget exhausted or LLM failed — use heuristic for entire batch
558
+ for (let j = 0; j < batch.length; j++) {
559
+ classifiedTypes[i + j] = batch[j].heuristicType;
560
+ heuristicFallback++;
561
+ }
562
+ }
563
+ // Phase C: Store all classified links
564
+ for (let i = 0; i < candidates.length; i++) {
565
+ const c = candidates[i];
566
+ const relationship = classifiedTypes[i];
567
+ if (!dryRun) {
568
+ try {
569
+ storage.addLink(db, c.source.id, c.target.id, relationship, c.similarity);
570
+ autoLinked++;
571
+ }
572
+ catch {
573
+ failed++;
574
+ }
575
+ }
576
+ else {
577
+ autoLinked++;
521
578
  }
522
579
  }
523
- return { auto_linked: autoLinked, failed };
580
+ return { auto_linked: autoLinked, llm_classified: llmClassified, heuristic_fallback: heuristicFallback, failed };
524
581
  }
525
582
  /**
526
583
  * Classify the relationship between two memories based on type, temporal ordering, and similarity.
@@ -660,8 +717,8 @@ async function runConsolidation(db, llm, embedFn, dryRun = false, skipReflection
660
717
  }
661
718
  // Stage 2.7: Domain Curation
662
719
  report.stages.domain_curation = await stageDomainCuration(db, llm, budget, dryRun, stateDir);
663
- // Stage 3: Link Discovery
664
- report.stages.links = await stageLinks(db, precheck.newMemories, embedFn, dryRun);
720
+ // Stage 3: Link Discovery (with LLM-assisted edge classification)
721
+ report.stages.links = await stageLinks(db, precheck.newMemories, embedFn, dryRun, llm, budget);
665
722
  // Stage 3.5: Hub Detection — boost highly-connected memories
666
723
  report.stages.hub_boost = stageHubBoost(db, dryRun);
667
724
  // Stage 4: Decay & Prune
package/dist/graph.d.ts CHANGED
@@ -50,5 +50,5 @@ export interface GraphNeighbor {
50
50
  content: string;
51
51
  project: string | null;
52
52
  }
53
- export declare function getNeighbors(db: Database.Database, memoryId: string, limit?: number): GraphNeighbor[];
53
+ export declare function getNeighbors(db: Database.Database, memoryId: string, limit?: number, relationship?: string): GraphNeighbor[];
54
54
  export declare function shortestPath(db: Database.Database, fromId: string, toId: string, maxDepth?: number): string[] | null;
package/dist/graph.js CHANGED
@@ -175,14 +175,20 @@ function detectHubs(db, thresholdMultiplier = 2, minLinks = 3) {
175
175
  }
176
176
  return hubs;
177
177
  }
178
- function getNeighbors(db, memoryId, limit = 10) {
179
- const rows = db
180
- .prepare(`SELECT source_id, target_id, relationship, strength
178
+ function getNeighbors(db, memoryId, limit = 10, relationship) {
179
+ let sql = `SELECT source_id, target_id, relationship, strength
181
180
  FROM memory_links
182
- WHERE source_id = ? OR target_id = ?
183
- ORDER BY strength DESC
184
- LIMIT ?`)
185
- .all(memoryId, memoryId, limit);
181
+ WHERE (source_id = ? OR target_id = ?)`;
182
+ const params = [memoryId, memoryId];
183
+ if (relationship) {
184
+ sql += ` AND relationship = ?`;
185
+ params.push(relationship);
186
+ }
187
+ sql += ` ORDER BY strength DESC LIMIT ?`;
188
+ params.push(limit);
189
+ const rows = db
190
+ .prepare(sql)
191
+ .all(...params);
186
192
  const results = [];
187
193
  for (const row of rows) {
188
194
  const isOutgoing = row.source_id === memoryId;
@@ -251,7 +251,8 @@ function createMcpServer() {
251
251
  target_id: zod_1.z.string().optional().describe("Target memory ID (required for path operation)"),
252
252
  limit: zod_1.z.coerce.number().optional().describe("Max results (default 10)"),
253
253
  domain: zod_1.z.string().optional().describe("Filter hubs by domain"),
254
- }, async ({ operation, id, target_id, limit: resultLimit, domain: filterDomain }) => {
254
+ relationship: zod_1.z.string().optional().describe("Filter neighbors by relationship type (e.g., CONTRADICTS, SUPERSEDES, derives)"),
255
+ }, async ({ operation, id, target_id, limit: resultLimit, domain: filterDomain, relationship: filterRelationship }) => {
255
256
  if (!db)
256
257
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
257
258
  try {
@@ -261,7 +262,7 @@ function createMcpServer() {
261
262
  const resolvedId = resolveMemoryId(db, id);
262
263
  if (!resolvedId)
263
264
  return { content: [{ type: "text", text: `Memory not found: ${id}` }], isError: true };
264
- const neighbors = (0, graph_js_1.getNeighbors)(db, resolvedId, resultLimit ?? 10);
265
+ const neighbors = (0, graph_js_1.getNeighbors)(db, resolvedId, resultLimit ?? 10, filterRelationship);
265
266
  if (neighbors.length === 0)
266
267
  return { content: [{ type: "text", text: "No connected memories found." }] };
267
268
  const text = neighbors.map((n) => `[${n.direction}] ${n.relationship} (${n.strength.toFixed(2)})\n ${n.id.slice(0, 8)} | ${n.project ?? "global"} | ${n.content}`).join("\n\n");
package/dist/prompts.d.ts CHANGED
@@ -19,3 +19,8 @@ export declare function distillation(projectName: string, date: string, transcri
19
19
  * Used during consolidation (Pro only, one call per nightly when projects change).
20
20
  */
21
21
  export declare function domainCuration(projectLines: string): string;
22
+ /**
23
+ * Edge classification prompt. Presents memory pairs and asks the LLM to
24
+ * choose the most specific relationship type for each.
25
+ */
26
+ export declare function edgeClassification(pairsBlock: string): string;
package/dist/prompts.js CHANGED
@@ -8,6 +8,7 @@ exports.importanceScoring = importanceScoring;
8
8
  exports.reflection = reflection;
9
9
  exports.distillation = distillation;
10
10
  exports.domainCuration = domainCuration;
11
+ exports.edgeClassification = edgeClassification;
11
12
  /**
12
13
  * Importance scoring prompt. Takes a {memories_block} with indexed memories.
13
14
  */
@@ -169,3 +170,31 @@ Rules:
169
170
 
170
171
  Respond with ONLY a JSON array. No explanations.`;
171
172
  }
173
+ /**
174
+ * Edge classification prompt. Presents memory pairs and asks the LLM to
175
+ * choose the most specific relationship type for each.
176
+ */
177
+ function edgeClassification(pairsBlock) {
178
+ return `You are a memory graph analyst. Classify the relationship between each memory pair.
179
+
180
+ VALID RELATIONSHIP TYPES:
181
+ - derives: A lesson or fact was derived from episodes (lesson ← episode)
182
+ - updates: A newer memory updates/replaces an older one on the same topic
183
+ - extends: Memory adds detail to another within the same project
184
+ - relates_to: Generic association (use ONLY when no specific type fits)
185
+ - CONTRADICTS: Memories give opposite advice or conflicting information
186
+ - SUPERSEDES: One memory fully replaces another (stronger than "updates")
187
+ - DEPENDS_ON: One memory's validity requires the other (prerequisite)
188
+ - CAUSED_BY: One event/decision directly caused the other
189
+ - VALIDATES: One memory confirms or provides evidence for the other
190
+
191
+ Choose the MOST SPECIFIC type. Prefer specific types over "relates_to".
192
+
193
+ MEMORY PAIRS:
194
+ ${pairsBlock}
195
+
196
+ Respond with ONLY a JSON array of relationship type strings, one per pair, in order.
197
+ Example for 3 pairs: ["CAUSED_BY", "extends", "VALIDATES"]
198
+
199
+ No explanations. Just the JSON array.`;
200
+ }
package/dist/types.d.ts CHANGED
@@ -27,6 +27,10 @@ export interface MemoryLink {
27
27
  strength: number;
28
28
  created_at: string;
29
29
  }
30
+ /** All valid relationship types for memory links.
31
+ * lowercase = heuristic (legacy), UPPER_SNAKE_CASE = LLM-classified (v0.7+). */
32
+ export declare const VALID_RELATIONSHIP_TYPES: readonly ["derives", "updates", "extends", "relates_to", "CONTRADICTS", "SUPERSEDES", "DEPENDS_ON", "CAUSED_BY", "VALIDATES"];
33
+ export type RelationshipType = typeof VALID_RELATIONSHIP_TYPES[number];
30
34
  /** A search result with scoring metadata. */
31
35
  export interface MemorySearchResult {
32
36
  id: string;
@@ -76,6 +80,8 @@ export interface ConsolidationReport {
76
80
  };
77
81
  links?: {
78
82
  auto_linked: number;
83
+ llm_classified?: number;
84
+ heuristic_fallback?: number;
79
85
  failed: number;
80
86
  };
81
87
  decay_prune?: {
package/dist/types.js CHANGED
@@ -4,3 +4,10 @@
4
4
  * Ported from the Python hicortex codebase.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.VALID_RELATIONSHIP_TYPES = void 0;
8
+ /** All valid relationship types for memory links.
9
+ * lowercase = heuristic (legacy), UPPER_SNAKE_CASE = LLM-classified (v0.7+). */
10
+ exports.VALID_RELATIONSHIP_TYPES = [
11
+ "derives", "updates", "extends", "relates_to",
12
+ "CONTRADICTS", "SUPERSEDES", "DEPENDS_ON", "CAUSED_BY", "VALIDATES",
13
+ ];
@@ -2,7 +2,7 @@
2
2
  "id": "hicortex",
3
3
  "name": "Hicortex — Long-term Memory That Learns",
4
4
  "description": "Your agents remember past decisions, avoid repeated mistakes, and get smarter every day. Nightly reflection generates actionable lessons that automatically update agent behavior.",
5
- "version": "0.7.0",
5
+ "version": "0.7.1",
6
6
  "kind": "lifecycle",
7
7
  "skills": ["./skills/hicortex-memory", "./skills/hicortex-learn", "./skills/hicortex-activate"],
8
8
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Human-like memory for self-improving AI agents. Automatic capturing, nightly reflection, and cross-agent learning. Works with Claude Code and OpenClaw.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {