@gamaze/hicortex 0.18.1 → 0.18.2

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.
@@ -238,10 +238,15 @@ let compKey = "by_type";
238
238
 
239
239
  const $ = (id) => document.getElementById(id);
240
240
 
241
- // #264 WS2: human-term labels for the internal memory_type enum. The data
242
- // blob carries the RAW enum keys (a stable JSON contract, also relied on by
243
- // tests); only the rendered label changes. Unknown keys pass through.
241
+ // #264 final: the data blob carries the CANONICAL human-term keys
242
+ // (knowledge/experience/decisions/learnings). Legacy raw keys are also mapped
243
+ // so a snapshot taken mid-migrate renders correctly. Unknown keys pass through.
244
244
  const TYPE_LABELS = {
245
+ knowledge: "Knowledge",
246
+ experience: "Experience",
247
+ decisions: "Decisions",
248
+ learnings: "Learnings",
249
+ // Legacy raw enum (same types, renamed).
245
250
  fact: "Knowledge",
246
251
  episode: "Experience",
247
252
  decision: "Decisions",
package/assets/viz.html CHANGED
@@ -389,10 +389,16 @@
389
389
  var selDomain = document.getElementById("f-domain");
390
390
  var selType = document.getElementById("f-type");
391
391
 
392
- // #264 WS2: human-term labels for the internal memory_type enum. Node data
393
- // carries the RAW enum (the /graph JSON contract filters on it); only the
394
- // rendered strings change. Unknown values pass through unchanged.
392
+ // #264 final: node data carries the CANONICAL human-term values
393
+ // (knowledge/experience/decisions/learnings). Legacy raw values are also
394
+ // mapped so a mid-migrate snapshot renders correctly. Unknown values pass
395
+ // through unchanged.
395
396
  var TYPE_LABELS = {
397
+ knowledge: "Knowledge",
398
+ experience: "Experience",
399
+ decisions: "Decisions",
400
+ learnings: "Learnings",
401
+ // Legacy raw enum (same types, renamed).
396
402
  fact: "Knowledge",
397
403
  episode: "Experience",
398
404
  decision: "Decisions",
@@ -418,7 +418,7 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
418
418
  const severity = String(lo.severity ?? "important");
419
419
  const confidence = String(lo.confidence ?? "medium");
420
420
  const sourcePattern = String(lo.source_pattern ?? "");
421
- // No `## Lesson:` prefix: memory_type='lesson' carries the type, and the
421
+ // No `## Lesson:` prefix: memory_type='learnings' carries the type, and the
422
422
  // text is the topic-first first line (display reads the first line, not a
423
423
  // header parse — see learnings-identity.ts / index.ts).
424
424
  let content = `${lessonText}\n\n`;
@@ -443,7 +443,7 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
443
443
  // the accidental 1−L2 scale and required cosine > 0.98 — the check
444
444
  // effectively never fired. See isContradictionCandidate.
445
445
  const similarLessons = storage.vectorSearch(db, embedding, 3)
446
- .filter((n) => isContradictionCandidate(n.distance) && n.memory_type === "lesson");
446
+ .filter((n) => isContradictionCandidate(n.distance) && n.memory_type === "learnings");
447
447
  let contradicted = false;
448
448
  if (similarLessons.length > 0 && budget.use("contradiction_check")) {
449
449
  const existingText = similarLessons[0].content.slice(0, 300);
@@ -470,7 +470,7 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
470
470
  storage.insertMemory(db, content, embedding, {
471
471
  sourceAgent: "hicortex/reflection",
472
472
  project,
473
- memoryType: "lesson",
473
+ memoryType: "learnings",
474
474
  baseStrength: baseStrength[severity] ?? 0.8,
475
475
  });
476
476
  generated++;
@@ -509,7 +509,7 @@ function rebuildContentModuleIndex(db, domains, stateDir) {
509
509
  .all();
510
510
  const lessonRows = db
511
511
  .prepare(`SELECT domain, COUNT(*) AS cnt FROM memories
512
- WHERE domain IS NOT NULL AND memory_type = 'lesson' GROUP BY domain`)
512
+ WHERE domain IS NOT NULL AND memory_type = 'learnings' GROUP BY domain`)
513
513
  .all();
514
514
  const memByDomain = new Map(memRows.map((r) => [r.domain, r.cnt]));
515
515
  const lessonByDomain = new Map(lessonRows.map((r) => [r.domain, r.cnt]));
@@ -637,7 +637,7 @@ async function stageDomainCuration(db, llm, budget, dryRun, stateDir) {
637
637
  }
638
638
  const lessonRows = db
639
639
  .prepare(`SELECT project, COUNT(*) as cnt FROM memories
640
- WHERE project IS NOT NULL AND memory_type = 'lesson'
640
+ WHERE project IS NOT NULL AND memory_type = 'learnings'
641
641
  GROUP BY project`)
642
642
  .all();
643
643
  const lessonsByProject = new Map(lessonRows.map((r) => [r.project, r.cnt]));
@@ -997,7 +997,7 @@ const SUPERSESSION_BATCH_SIZE = 500;
997
997
  * events, not mutable state, so there is nothing to supersede.
998
998
  */
999
999
  function isSupersedableShape(mem) {
1000
- return (mem.memory_type === "decision" ||
1000
+ return (mem.memory_type === "decisions" ||
1001
1001
  mem.content.includes("[Decisions Made]") ||
1002
1002
  mem.content.includes("[Corrections & Rejections]") ||
1003
1003
  mem.content.includes("[Facts Learned]") ||
@@ -1117,7 +1117,7 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
1117
1117
  // in lockstep (an inline SQL copy, so drift here silently narrows scope).
1118
1118
  `SELECT rowid AS __rowid, * FROM memories
1119
1119
  WHERE rowid > ?
1120
- AND (memory_type = 'decision'
1120
+ AND (memory_type = 'decisions'
1121
1121
  OR content LIKE '%[Decisions Made]%'
1122
1122
  OR content LIKE '%[Corrections & Rejections]%'
1123
1123
  OR content LIKE '%[Facts Learned]%'
package/dist/dashboard.js CHANGED
@@ -53,7 +53,7 @@ function countBy(db, col) {
53
53
  function computeDashboardMetrics(db) {
54
54
  const mem = db.prepare("SELECT COUNT(*) AS c FROM memories").get().c;
55
55
  const lesson = db
56
- .prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = 'lesson'")
56
+ .prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = 'learnings'")
57
57
  .get().c;
58
58
  const link = db.prepare("SELECT COUNT(*) AS c FROM memory_links").get().c;
59
59
  const adoptionRow = db
@@ -228,7 +228,7 @@ function backfillSnapshots(db) {
228
228
  const dayRows = byDay.get(d);
229
229
  for (const r of dayRows) {
230
230
  mem++;
231
- if (r.memory_type === "lesson")
231
+ if (r.memory_type === "learnings")
232
232
  lesson++;
233
233
  byType[r.memory_type] = (byType[r.memory_type] ?? 0) + 1;
234
234
  const domKey = r.domain ?? "(unscoped)";
@@ -380,7 +380,7 @@ function handleDashboardData(db, query, config) {
380
380
  const lessonRows = db
381
381
  .prepare(`SELECT id, content, created_at
382
382
  FROM memories
383
- WHERE memory_type = 'lesson' AND created_at BETWEEN ? AND ?
383
+ WHERE memory_type = 'learnings' AND created_at BETWEEN ? AND ?
384
384
  ORDER BY created_at ASC`)
385
385
  .all(dayStart, dayEnd);
386
386
  // Stage outcomes for the day: dedup merges + supersession links that day.
package/dist/db.js CHANGED
@@ -105,7 +105,7 @@ CREATE TABLE IF NOT EXISTS memories (
105
105
  source_session TEXT,
106
106
  project TEXT,
107
107
  privacy TEXT DEFAULT 'WORK',
108
- memory_type TEXT DEFAULT 'episode',
108
+ memory_type TEXT DEFAULT 'experience',
109
109
  updated_at TIMESTAMP
110
110
  );
111
111
 
@@ -488,6 +488,29 @@ const MIGRATIONS = [
488
488
  `);
489
489
  },
490
490
  },
491
+ {
492
+ version: 13,
493
+ name: "memory_type_unified_terminology",
494
+ up: (db) => {
495
+ // #264 final step: rename the memory_type COLUMN VALUES from the raw
496
+ // internal enum (fact/episode/decision/lesson) to the unified human
497
+ // terms (knowledge/experience/decisions/learnings). The column itself,
498
+ // its default, and every SQL query/filter were updated in the same
499
+ // change; this migration converts existing rows in place. Idempotent:
500
+ // re-running against an already-migrated DB matches 0 rows per clause.
501
+ // The CREATE TABLE default is now 'experience' (was 'episode'); legacy
502
+ // rows with the old default value are rewritten here.
503
+ //
504
+ // Ordering is irrelevant (each clause keys on a distinct old value) and
505
+ // no clause can fire on another clause's output (the new values are
506
+ // disjoint from the old). The transaction wrapper in migrate() makes the
507
+ // whole migration atomic.
508
+ db.exec("UPDATE memories SET memory_type = 'knowledge' WHERE memory_type = 'fact'");
509
+ db.exec("UPDATE memories SET memory_type = 'experience' WHERE memory_type = 'episode'");
510
+ db.exec("UPDATE memories SET memory_type = 'decisions' WHERE memory_type = 'decision'");
511
+ db.exec("UPDATE memories SET memory_type = 'learnings' WHERE memory_type = 'lesson'");
512
+ },
513
+ },
491
514
  ];
492
515
  /**
493
516
  * Run all pending migrations against the database.
@@ -64,13 +64,13 @@ export declare function distillSession(llm: LlmClient, conversation: string, pro
64
64
  export declare function hasMinimalSubstance(entry: string): boolean;
65
65
  /**
66
66
  * A parsed distillation entry: the stored content (type tag STRIPPED) plus the
67
- * classified memory_type. `memoryType` is one of "episode" | "fact" |
68
- * "decision" — the three distillation-time types. "lesson" is deliberately
69
- * absent: lessons are the reflection stage's product, never distillation's
70
- * (#216). A missing/unknown tag defaults to "episode" so older distiller
67
+ * classified memory_type. `memoryType` is one of "experience" | "knowledge" |
68
+ * "decisions" — the three distillation-time types. "learnings" is deliberately
69
+ * absent: learnings are the reflection stage's product, never distillation's
70
+ * (#216). A missing/unknown tag defaults to "experience" so older distiller
71
71
  * output (pre-#216, no tag) stays backward-compatible.
72
72
  */
73
73
  export interface DistilledEntry {
74
74
  content: string;
75
- memoryType: "episode" | "fact" | "decision";
75
+ memoryType: "experience" | "knowledge" | "decisions";
76
76
  }
package/dist/distiller.js CHANGED
@@ -267,7 +267,7 @@ async function distillSession(llm, conversation, projectName, date, chunkSizeCha
267
267
  for (const entry of entries) {
268
268
  // Deduplicate by normalized content (type tag does not participate —
269
269
  // two chunks extracting the same fact should collapse regardless of
270
- // whether one tagged it [F] and the other [E]).
270
+ // whether one tagged it [K] and the other [E]).
271
271
  const key = entry.content.toLowerCase().replace(/\s+/g, " ").slice(0, 100);
272
272
  if (!seen.has(key)) {
273
273
  seen.add(key);
@@ -325,7 +325,7 @@ async function distillChunk(llm, transcript, projectName, date) {
325
325
  // sometimes ignore constraints (cf. the prior max-15-bullet failure). Count
326
326
  // entries that still look actor-led or bracket-led so a format regression
327
327
  // shows in nightly logs, not months later in the next eval. Non-blocking.
328
- // Note: the type tag ([E]/[F]/[D]) is already stripped by the parser, so a
328
+ // Note: the type tag ([E]/[K]/[D]) is already stripped by the parser, so a
329
329
  // leading bracket here means a payload-bracket or a category-first regression.
330
330
  const offTopic = parsed.filter((e) => /^\s*(user|ai|the user|assistant)\b/i.test(e.content) || /^\s*\[/.test(e.content)).length;
331
331
  if (parsed.length > 0 && offTopic > 0) {
@@ -426,21 +426,31 @@ function hasMinimalSubstance(entry) {
426
426
  }
427
427
  /**
428
428
  * Map a single-letter type tag to the stored memory_type. Unknown/absent →
429
- * episode (the pre-#216 default). `[L]` is explicitly rejected → episode: the
430
- * distiller must NEVER emit lessons (that's the reflection stage's job), so a
431
- * model that emits `[L]` is wrong and we do not propagate it as a lesson.
429
+ * experience (the pre-#216 default). `[L]` is explicitly rejected →
430
+ * experience: the distiller must NEVER emit learnings (that's the reflection
431
+ * stage's job), so a model that emits `[L]` is wrong and we do not propagate
432
+ * it as a learning.
433
+ *
434
+ * The single-letter tags ([E]/[K]/[D]) are unchanged from the raw-enum era —
435
+ * the model is taught these as "EXPERIENCE/KNOWLEDGE/DECISIONS" concepts in prompts.ts
436
+ * (ordinary English the model understands), and only the resulting STORED
437
+ * value changed in #264 (episode→experience, fact→knowledge, decision→
438
+ * decisions). The tag letters stay stable so neither the prompt nor the
439
+ * parser needs to change; only this mapping table moves.
432
440
  */
433
441
  function typeFromTag(letter) {
434
442
  switch (letter) {
435
- case "F":
443
+ case "K":
444
+ case "k":
445
+ case "F": // legacy tag (was Fact)
436
446
  case "f":
437
- return "fact";
447
+ return "knowledge";
438
448
  case "D":
439
449
  case "d":
440
- return "decision";
441
- // E, e, L, l (rejected), undefined, or anything else → episode.
450
+ return "decisions";
451
+ // E, e, L, l (rejected), undefined, or anything else → experience.
442
452
  default:
443
- return "episode";
453
+ return "experience";
444
454
  }
445
455
  }
446
456
  /**
@@ -448,7 +458,7 @@ function typeFromTag(letter) {
448
458
  * Each bullet becomes a separate memory. The leading `[E]`/`[F]`/`[D]` type
449
459
  * tag is extracted (→ memoryType), stripped from the stored content, and
450
460
  * passed to `insertMemory` via the `memoryType` option (#216). Bullets with
451
- * no tag default to "episode" (backward compatible with pre-#216 distiller
461
+ * no tag default to "experience" (backward compatible with pre-#216 distiller
452
462
  * output that never carried a tag).
453
463
  */
454
464
  function parseDistilledEntries(markdown) {
@@ -472,15 +482,15 @@ function parseDistilledEntries(markdown) {
472
482
  // Extract an optional leading single-letter type tag: "[E]", "[F]",
473
483
  // "[D]" (case-insensitive). The tag must be the very first token of the
474
484
  // bullet — a bracket that appears later is payload, not a type tag.
475
- const tagMatch = body.match(/^\[([EFDefdLl])\]\s*/);
485
+ const tagMatch = body.match(/^\[([EFDKefdklL])\]\s*/);
476
486
  if (tagMatch) {
477
487
  const memoryType = typeFromTag(tagMatch[1].toUpperCase());
478
488
  entries.push({ content: body.slice(tagMatch[0].length), memoryType });
479
489
  }
480
490
  else {
481
- // No tag → episode (pre-#216 distiller output, or a model that
491
+ // No tag → experience (pre-#216 distiller output, or a model that
482
492
  // skipped the tag). Keep the content verbatim.
483
- entries.push({ content: body, memoryType: "episode" });
493
+ entries.push({ content: body, memoryType: "experience" });
484
494
  }
485
495
  }
486
496
  }
@@ -431,7 +431,7 @@ async function buildCorpusDb(dbPath) {
431
431
  const vec = await (0, embedder_js_1.embed)(mem.text);
432
432
  const id = storage.insertMemory(db, mem.text, vec, {
433
433
  sourceAgent: "eval-corpus",
434
- memoryType: "episode",
434
+ memoryType: "experience",
435
435
  baseStrength: 0.5, // uniform — strength is NOT a discriminator here
436
436
  });
437
437
  idToTopic.set(id, mem.topic);
@@ -455,7 +455,7 @@ async function buildScopeDb(dbPath) {
455
455
  const vec = await (0, embedder_js_1.embed)(mem.text);
456
456
  const id = storage.insertMemory(db, mem.text, vec, {
457
457
  sourceAgent: "eval-scope",
458
- memoryType: "episode",
458
+ memoryType: "experience",
459
459
  baseStrength: 0.5, // uniform — strength is NOT a discriminator here
460
460
  project: mem.project, // #203 scope label
461
461
  });
@@ -11,11 +11,11 @@ exports.runReflectionCensus = runReflectionCensus;
11
11
  function runReflectionCensus(db) {
12
12
  const lessonsByDate = db
13
13
  .prepare(`SELECT date(created_at) AS date, COUNT(*) AS count
14
- FROM memories WHERE memory_type = 'lesson'
14
+ FROM memories WHERE memory_type = 'learnings'
15
15
  GROUP BY date ORDER BY date`)
16
16
  .all();
17
- const totalLessons = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = 'lesson'").get().c;
18
- const totalEpisodes = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = 'episode'").get().c;
17
+ const totalLessons = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = 'learnings'").get().c;
18
+ const totalEpisodes = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = 'experience'").get().c;
19
19
  return {
20
20
  lessonsByDate,
21
21
  totalLessons,
package/dist/index.js CHANGED
@@ -150,7 +150,7 @@ async function fetchOcIdentityBlock(agentId) {
150
150
  return (0, learnings_identity_js_1.gateAndRenderIdentity)(data, THIS_HARNESS, { requireAgentEcho: agentId !== null });
151
151
  }
152
152
  /**
153
- * Fetch /lessons and build the `## Hicortex Lessons` block, or null on any
153
+ * Fetch /lessons and build the `## Hicortex Learnings` block, or null on any
154
154
  * failure or when no lessons survive selection. Preserves the pre-0.13 lesson
155
155
  * output; the caller prepends the `## Identity` block and adds separators.
156
156
  */
@@ -177,8 +177,8 @@ async function buildLessonsBlock(project) {
177
177
  const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
178
178
  return `- ${title}${meta ? ` (${meta})` : ""}`;
179
179
  });
180
- return (`## Hicortex Lessons (auto-injected from long-term memory)\n` +
181
- `These are actionable lessons learned from past sessions:\n\n` +
180
+ return (`## Hicortex Learnings (auto-injected from long-term memory)\n` +
181
+ `These are actionable Learnings from past sessions:\n\n` +
182
182
  formatted.join("\n"));
183
183
  }
184
184
  // ---------------------------------------------------------------------------
@@ -354,7 +354,7 @@ exports.default = {
354
354
  const agentId = (0, identity_store_js_1.sanitizeAgentId)(ctx?.agentId ?? "");
355
355
  // Fetch all three concurrently with INDEPENDENT fail-soft: no block
356
356
  // may ever cost another. Order in the injected output: `## Identity`
357
- // (standing identity, 0.13) → `## Hicortex Lessons` → the per-turn
357
+ // (standing identity, 0.13) → `## Hicortex Learnings` → the per-turn
358
358
  // `## Memory recall (auto)` index (#193, closest to the prompt).
359
359
  const [identityBlock, lessonsBlock, recallBlock] = await Promise.all([
360
360
  fetchOcIdentityBlock(agentId).catch(() => null),
@@ -481,7 +481,7 @@ exports.default = {
481
481
  }), { name: "hicortex_recent" });
482
482
  api.registerTool((_ctx) => ({
483
483
  name: "hicortex_ingest",
484
- description: "Store a new memory in long-term storage. Use for important facts, decisions, or lessons.",
484
+ description: "Store a new memory in long-term storage. Use for Knowledge, Decisions, or Learnings.",
485
485
  parameters: {
486
486
  type: "object",
487
487
  properties: {
@@ -489,8 +489,8 @@ exports.default = {
489
489
  project: { type: "string", description: "Project this memory belongs to" },
490
490
  memory_type: {
491
491
  type: "string",
492
- enum: ["episode", "lesson", "fact", "decision"],
493
- description: "Type of memory (default: episode)",
492
+ enum: ["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"],
493
+ description: "Type of memory (default: Experience). Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term).",
494
494
  },
495
495
  },
496
496
  required: ["content"],
@@ -501,7 +501,7 @@ exports.default = {
501
501
  content: args.content,
502
502
  source_agent: `openclaw/${context?.agentId ?? "manual"}`,
503
503
  project: args.project,
504
- memory_type: args.memory_type ?? "episode",
504
+ memory_type: args.memory_type ? (0, type_labels_js_1.normalizeMemoryType)(args.memory_type) : "experience",
505
505
  }, 15000);
506
506
  if (!result.ok) {
507
507
  return { error: `Ingest failed: ${result.data?.error ?? `HTTP ${result.status}`}` };
@@ -516,7 +516,7 @@ exports.default = {
516
516
  }), { name: "hicortex_ingest" });
517
517
  api.registerTool((_ctx) => ({
518
518
  name: "hicortex_lessons",
519
- description: "Get actionable lessons learned from past sessions. Auto-generated insights about mistakes to avoid.",
519
+ description: "Get actionable Learnings distilled from past sessions. Auto-generated insights about mistakes to avoid.",
520
520
  parameters: {
521
521
  type: "object",
522
522
  properties: {
@@ -530,7 +530,7 @@ exports.default = {
530
530
  return { error: `Lessons fetch failed: ${describeGetFailure(status, "/lessons")}` };
531
531
  const lessons = data.lessons ?? [];
532
532
  if (lessons.length === 0) {
533
- return { content: [{ type: "text", text: "No lessons found." }] };
533
+ return { content: [{ type: "text", text: "No Learnings found." }] };
534
534
  }
535
535
  const text = lessons.map((l) => `- ${l.content.slice(0, 500)}`).join("\n");
536
536
  return { content: [{ type: "text", text }] };
@@ -612,8 +612,8 @@ exports.default = {
612
612
  project: { type: "string", description: "New project name" },
613
613
  memory_type: {
614
614
  type: "string",
615
- enum: ["episode", "lesson", "fact", "decision"],
616
- description: "New memory type",
615
+ enum: ["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"],
616
+ description: "New memory type. Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term).",
617
617
  },
618
618
  },
619
619
  required: ["id"],
@@ -624,7 +624,7 @@ exports.default = {
624
624
  id: args.id,
625
625
  content: args.content,
626
626
  project: args.project,
627
- memory_type: args.memory_type,
627
+ memory_type: args.memory_type ? (0, type_labels_js_1.normalizeMemoryType)(args.memory_type) : undefined,
628
628
  }, 15000);
629
629
  if (result.status === 404) {
630
630
  return { error: `Memory not found: ${args.id}` };
@@ -117,7 +117,7 @@ async function fetchLessonsBlock(cfg) {
117
117
  parts.push("BEFORE making decisions, search memory: `hicortex_search` for prior decisions on the same topic.");
118
118
  parts.push("Use `hicortex_recent` at session start for recent project state.");
119
119
  if (lessonLines.length > 0) {
120
- parts.push("", "### Lessons (updated nightly)");
120
+ parts.push("", "### Learnings (updated nightly)");
121
121
  parts.push(...lessonLines);
122
122
  }
123
123
  const { index } = data;
@@ -125,16 +125,16 @@ async function fetchLessonsBlock(cfg) {
125
125
  parts.push("", "### Memory Index");
126
126
  for (const domain of moduleIndex.domains) {
127
127
  const kwStr = domain.keywords.length > 0 ? `: ${domain.keywords.join(", ")}` : "";
128
- parts.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount} lessons)${kwStr}`);
128
+ parts.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount} Learnings)${kwStr}`);
129
129
  if (domain.projects.length > 0)
130
130
  parts.push(` ${domain.projects.join(" | ")}`);
131
131
  }
132
- parts.push(`${index.total} memories, ${index.lessonCount} lessons, ${index.sourceCount} agents. Search with \`hicortex_search\`.`);
132
+ parts.push(`${index.total} memories, ${index.lessonCount} Learnings, ${index.sourceCount} agents. Search with \`hicortex_search\`.`);
133
133
  }
134
134
  else if (index.projects.length > 0) {
135
135
  parts.push("", "### Memory Index");
136
136
  parts.push(index.projects.map(p => `${p.name}: ${p.count}`).join(" | "));
137
- parts.push(`${index.total} memories, ${index.lessonCount} lessons, ${index.sourceCount} agents. Search with \`hicortex_search\`.`);
137
+ parts.push(`${index.total} memories, ${index.lessonCount} Learnings, ${index.sourceCount} agents. Search with \`hicortex_search\`.`);
138
138
  }
139
139
  return parts.join("\n");
140
140
  }
@@ -183,10 +183,10 @@ function createMcpServer() {
183
183
  }
184
184
  });
185
185
  // -- hicortex_ingest --
186
- server.tool("hicortex_ingest", "Store a new memory in long-term storage. Use for important facts, decisions, or lessons.", {
186
+ server.tool("hicortex_ingest", "Store a new memory in long-term storage. Use for Knowledge, Decisions, or Learnings.", {
187
187
  content: zod_1.z.string().describe("Memory content to store"),
188
188
  project: zod_1.z.string().optional().describe("Project this memory belongs to"),
189
- memory_type: zod_1.z.enum(["episode", "lesson", "fact", "decision"]).optional().describe("Type of memory (default: episode)"),
189
+ memory_type: zod_1.z.enum(["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"]).optional().describe("Type of memory (default: Experience). Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term)."),
190
190
  }, async ({ content, project, memory_type }) => {
191
191
  if (!db)
192
192
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
@@ -195,7 +195,8 @@ function createMcpServer() {
195
195
  const id = storage.insertMemory(db, content, embedding, {
196
196
  sourceAgent: "claude-code/manual",
197
197
  project,
198
- memoryType: memory_type ?? "episode",
198
+ // Normalize legacy raw enum to the canonical term the DB stores.
199
+ memoryType: memory_type ? (0, type_labels_js_1.normalizeMemoryType)(memory_type) : "experience",
199
200
  });
200
201
  return { content: [{ type: "text", text: `Memory stored (id: ${id.slice(0, 8)})` }] };
201
202
  }
@@ -208,7 +209,7 @@ function createMcpServer() {
208
209
  id: zod_1.z.string().describe("Memory ID (from search results, first 8 chars or full UUID)"),
209
210
  content: zod_1.z.string().optional().describe("New content text"),
210
211
  project: zod_1.z.string().optional().describe("New project name"),
211
- memory_type: zod_1.z.enum(["episode", "lesson", "fact", "decision"]).optional().describe("New memory type"),
212
+ memory_type: zod_1.z.enum(["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"]).optional().describe("New memory type. Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term)."),
212
213
  }, async ({ id, content, project, memory_type }) => {
213
214
  if (!db)
214
215
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
@@ -222,8 +223,9 @@ function createMcpServer() {
222
223
  fields.content = content;
223
224
  if (project !== undefined)
224
225
  fields.project = project;
226
+ // Normalize legacy raw enum to canonical human terms before DB write.
225
227
  if (memory_type !== undefined)
226
- fields.memory_type = memory_type;
228
+ fields.memory_type = (0, type_labels_js_1.normalizeMemoryType)(memory_type);
227
229
  if (Object.keys(fields).length === 0) {
228
230
  return { content: [{ type: "text", text: "No fields to update" }], isError: true };
229
231
  }
@@ -319,7 +321,7 @@ function createMcpServer() {
319
321
  const moduleIndex = state.moduleIndex;
320
322
  if (moduleIndex && moduleIndex.domains.length > 0) {
321
323
  const text = moduleIndex.domains.map((d) => {
322
- const head = `**${d.name}** (${d.memoryCount} memories, ${d.lessonCount} lessons)`;
324
+ const head = `**${d.name}** (${d.memoryCount} memories, ${d.lessonCount} Learnings)`;
323
325
  // Content-based domains carry a description and no projects; legacy
324
326
  // project-grouping domains carry a project list + keywords.
325
327
  if (d.description && d.projects.length === 0) {
@@ -692,11 +694,15 @@ async function startServer(options = {}) {
692
694
  res.status(400).json({ error: "Missing or invalid 'content' field" });
693
695
  return;
694
696
  }
695
- const validTypes = ["episode", "lesson", "fact", "decision"];
697
+ const validTypes = type_labels_js_1.ACCEPTED_MEMORY_TYPES;
696
698
  if (memory_type && !validTypes.includes(memory_type)) {
697
699
  res.status(400).json({ error: `Invalid memory_type: ${memory_type}` });
698
700
  return;
699
701
  }
702
+ // Normalize legacy raw enum (fact/episode/decision/lesson) to the
703
+ // canonical term the DB stores (knowledge/experience/decisions/learnings).
704
+ // Canonical values pass through unchanged.
705
+ const normalizedType = memory_type ? (0, type_labels_js_1.normalizeMemoryType)(memory_type) : memory_type;
700
706
  // Dedup by source_session (idempotent — skip if already ingested)
701
707
  if (source_session) {
702
708
  const existing = db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE source_session = ?").get(source_session);
@@ -714,7 +720,7 @@ async function startServer(options = {}) {
714
720
  sourceDomain: typeof source_domain === "string" ? source_domain : null,
715
721
  sourceSession: source_session ?? undefined,
716
722
  project: project ?? undefined,
717
- memoryType: memory_type ?? "episode",
723
+ memoryType: normalizedType ?? "experience",
718
724
  // 0.16.x: privacy defaults to null (vestigial column). A legacy client
719
725
  // that sends an explicit value is honored; absent → null.
720
726
  privacy: typeof privacy === "string" ? privacy : null,
@@ -1061,9 +1067,9 @@ async function startServer(options = {}) {
1061
1067
  sourceSession: sourcePrefix ? `${sourcePrefix}#${i}` : undefined,
1062
1068
  project: project ?? undefined,
1063
1069
  // #216: the distiller now classifies each entry as
1064
- // episode/fact/decision via the [E]/[F]/[D] tag parsed in
1070
+ // experience/knowledge/decisions via the [E]/[K]/[D] tag parsed in
1065
1071
  // distiller.ts. Pre-#216 distiller output (no tag) defaults to
1066
- // episode in the parser, so this is backward compatible.
1072
+ // experience in the parser, so this is backward compatible.
1067
1073
  memoryType,
1068
1074
  // 0.16.x: privacy defaults to null (vestigial column). A legacy
1069
1075
  // client that sends an explicit value is honored; absent → null.
@@ -1111,19 +1117,24 @@ async function startServer(options = {}) {
1111
1117
  fields.content = content;
1112
1118
  if (project !== undefined)
1113
1119
  fields.project = project;
1114
- if (memory_type !== undefined)
1115
- fields.memory_type = memory_type;
1116
1120
  if (privacy !== undefined)
1117
1121
  fields.privacy = privacy;
1118
- if (Object.keys(fields).length === 0) {
1119
- res.status(400).json({ error: "No fields to update" });
1120
- return;
1121
- }
1122
- const validTypes = ["episode", "lesson", "fact", "decision"];
1122
+ // Validate + normalize memory_type BEFORE adding to `fields` so the
1123
+ // empty-fields check below correctly counts a memory_type-only update.
1124
+ const validTypes = type_labels_js_1.ACCEPTED_MEMORY_TYPES;
1123
1125
  if (memory_type !== undefined && !validTypes.includes(memory_type)) {
1124
1126
  res.status(400).json({ error: `Invalid memory_type: ${memory_type}` });
1125
1127
  return;
1126
1128
  }
1129
+ // Normalize legacy raw enum (fact/episode/decision/lesson) to the
1130
+ // canonical term the DB stores (knowledge/experience/decisions/learnings).
1131
+ // Canonical values pass through unchanged.
1132
+ if (memory_type !== undefined)
1133
+ fields.memory_type = (0, type_labels_js_1.normalizeMemoryType)(memory_type);
1134
+ if (Object.keys(fields).length === 0) {
1135
+ res.status(400).json({ error: "No fields to update" });
1136
+ return;
1137
+ }
1127
1138
  try {
1128
1139
  storage.updateMemory(db, fullId, fields);
1129
1140
  // Re-embed when content changes
package/dist/prompts.js CHANGED
@@ -48,7 +48,7 @@ function reflection(memoriesBlock, recentLessons) {
48
48
  Like human learning: we grow fastest when we reinforce what works AND correct what doesn't. A system that only learns from mistakes becomes overly cautious. A system that only learns from successes never improves. The combination multiplies.
49
49
 
50
50
  GENERALITY BAR (read carefully — the most important rule):
51
- Every lesson MUST be a generalizable operating principle that transfers across contexts, agents, and projects. It is NOT: an incident report, a changelog entry, a one-event fact, a tool-specific recipe, or a note about a named entity. If a memory is only interesting as "what happened today", it is an EPISODE — do not emit a lesson for it. Abstract away specific tool names, hostnames, and incident details from the lesson text; state the transferable rule.
51
+ Every lesson MUST be a generalizable operating principle that transfers across contexts, agents, and projects. It is NOT: an incident report, a changelog entry, a one-event fact, a tool-specific recipe, or a note about a named entity. If a memory is only interesting as "what happened today", it is an EXPERIENCE — do not emit a lesson for it. Abstract away specific tool names, hostnames, and incident details from the lesson text; state the transferable rule.
52
52
 
53
53
  Quality over quantity. 1-3 lessons is typical. An empty array [] is the CORRECT response when memories show routine competent work without noteworthy patterns, surprises, or friction. Do not manufacture lessons from nothing.
54
54
 
@@ -121,8 +121,8 @@ EXTRACT into this markdown format:
121
121
  ### Decisions Made
122
122
  - [D] [SUBJECT]: [decision] — [reasoning] (${date})
123
123
 
124
- ### Facts Learned
125
- - [F] [SUBJECT]: [fact] — [context/source] (${date})
124
+ ### Knowledge Learned
125
+ - [K] [SUBJECT]: [knowledge] — [context/source] (${date})
126
126
 
127
127
  ### Problems & Solutions
128
128
  - [E] [SUBJECT]: [problem] → [solution that worked] (${date})
@@ -131,7 +131,7 @@ EXTRACT into this markdown format:
131
131
  - [D] [SUBJECT]: [what changed], [from → to] (${date})
132
132
 
133
133
  ### Key Entities & Relationships
134
- - [F] [entity A] → [relationship] → [entity B] (${date})
134
+ - [K] [entity A] → [relationship] → [entity B] (${date})
135
135
 
136
136
  ### Corrections & Rejections
137
137
  - [E] [SUBJECT]: [what AI proposed] → [why rejected/corrected] → [what user wanted instead] (${date})
@@ -139,18 +139,18 @@ EXTRACT into this markdown format:
139
139
  user corrections of AI assumptions, quality complaints like "too verbose")
140
140
 
141
141
  TYPE TAG (critical — prefix EVERY bullet with exactly one letter + space):
142
- - [E] EPISODE — a specific event, interaction, or narrative: "tried X, failed
142
+ - [E] EXPERIENCE — a specific event, interaction, or narrative: "tried X, failed
143
143
  because Y", a correction, a debugging session, a one-time occurrence. The
144
144
  DEFAULT when in doubt.
145
- - [F] FACT — a durable truth that will hold across sessions: "the API is at
145
+ - [K] KNOWLEDGE — a durable truth that will hold across sessions: "the API is at
146
146
  :8787", "uv is used for packages", "config lives in ~/.hicortex/". Not tied
147
147
  to a single moment.
148
- - [D] DECISION — a choice made that future work builds on, and that a later
148
+ - [D] DECISIONS — a choice made that future work builds on, and that a later
149
149
  decision can SUPERSEDE: "switched from gemma4 to qwen3.5", "adopted the
150
- graded-schema tag model". Not a fact (it can change) and not an episode
150
+ graded-schema tag model". Not knowledge (it can change) and not experience
151
151
  (it persists and constrains).
152
- - NEVER use [L] (lesson). Lessons are extracted by a SEPARATE reflection stage,
153
- not here. If the model emits [L], it is wrong — re-tag as episode/fact/decision.
152
+ - NEVER use [L] (learnings). Learnings are extracted by a SEPARATE reflection stage,
153
+ not here. If the model emits [L], it is wrong — re-tag as experience/knowledge/decisions.
154
154
  The type tag goes BEFORE the subject, never as a section/category bracket.
155
155
 
156
156
  TOPIC-FIRST RULE (critical — read carefully):
@@ -159,8 +159,8 @@ concrete thing it is about — the system, file, component, decision area, or
159
159
  entity. The subject is what a future reader would search for.
160
160
  - Write: "[E] Electrical load calculation: don't bundle unknown loads into one figure — user rejected the estimate"
161
161
  - NOT: "[E] User rejected AI's bundling of unknown loads"
162
- - Write: "[F] Nightly capture (Hermes): cron sessions are excluded — source='cron' is skipped before distillation"
163
- - NOT: "[F] Discovered that cron sessions are filtered out"
162
+ - Write: "[K] Nightly capture (Hermes): cron sessions are excluded — source='cron' is skipped before distillation"
163
+ - NOT: "[K] Discovered that cron sessions are filtered out"
164
164
  Reason: each item's first words (after the type tag) become the memory's one-line
165
165
  index entry AND dominate its search embedding. An item that opens with a category
166
166
  label, a sentiment ("Strong Negative"), or "User rejected…" is unfindable — it