@gamaze/hicortex 0.18.0 → 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.
package/README.md CHANGED
@@ -118,17 +118,18 @@ The run is resumable — interrupt it any time and it continues where it stopped
118
118
 
119
119
  ## Agent Tools (MCP)
120
120
 
121
- 9 tools available via MCP:
121
+ 10 canonical tools available via MCP (plus `hicortex_lessons` as a backcompat alias of `hicortex_learnings`):
122
122
 
123
123
  - **hicortex_search** — Semantic search across all stored memories
124
124
  - **hicortex_get** — Fetch one memory's full content by id (0.14) — the lazy-load counterpart of the recall index; fetching marks the memory as used
125
125
  - **hicortex_recent** — Get recent decisions and project state (queryless recall; renamed in 0.12)
126
126
  - **hicortex_ingest** — Store a memory directly
127
- - **hicortex_lessons** — Get actionable lessons from reflection
127
+ - **hicortex_learnings** — Get actionable Learnings from reflection (`hicortex_lessons` is kept as a backcompat alias)
128
128
  - **hicortex_index** — Get the knowledge domain index (what topics are stored)
129
129
  - **hicortex_graph** — Graph traversal: neighbors, hubs, shortest paths
130
130
  - **hicortex_update** — Fix incorrect memories (re-embeds on content change)
131
131
  - **hicortex_delete** — Remove memories with cascade cleanup
132
+ - **hicortex_identity** — Fetch your standing identity layer on demand (all sections, or one by name; pass `agent` on multi-agent installs to scope to a specific agent)
132
133
 
133
134
  Explicit learnings: call `hicortex_ingest` directly (capture is otherwise automatic, nightly).
134
135
 
@@ -271,7 +272,7 @@ Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze
271
272
  | `/recent` | GET | Yes | Recent memories, queryless recall (renamed from `/context` in 0.12) |
272
273
  | `/identity` | GET / PUT | Yes | Standing [identity layer](#identity-layer): read all sections / partial-upsert named sections. `?agent=<id>` selects a [per-agent scope](#per-agent-identity-013) (server resolves override/global/off + merge); invalid id → 400. Recall-style query params on GET → 400 (use `/recent`). (`/context` remains as a backcompat alias.) |
273
274
  | `/identity/ui` | GET | No* | Web editor for the identity layer (shell served without auth, like `/viz`; data via `/identity`) |
274
- | `/lessons` | GET | Yes | Lessons + memory index (used by CC SessionStart hook) |
275
+ | `/learnings` | GET | Yes | Learnings + memory index (used by CC SessionStart hook). `/lessons` is a backcompat alias for the same handler |
275
276
  | `/ingest` | POST | Yes | Legacy: accept a single pre-distilled memory from older clients |
276
277
  | `/sse` | GET | Yes | MCP SSE stream for agent connections |
277
278
  | `/messages` | POST | Yes | MCP message endpoint |
@@ -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}` };
@@ -26,6 +26,7 @@
26
26
  * parse error) results in silent exit-0. A broken hook must never block a
27
27
  * CC session, and a broken /identity fetch must never blank the whole output.
28
28
  */
29
+ import { type AgentMode } from "./identity-store.js";
29
30
  /**
30
31
  * The GET /identity response shape, shared by the CC hook and the OC plugin so
31
32
  * their gating cannot drift. `agent`/`mode` are echoed by a 0.13 server whenever
@@ -95,6 +96,41 @@ export declare function renderIdentityBlock(sections: Record<string, string>): s
95
96
  * global IS the operator's intended state there, and a guard would instead
96
97
  * blank ALL identity for every CC session in that window.
97
98
  */
99
+ /**
100
+ * Result of `buildIdentityToolResult` — the MCP tool handler maps this to its
101
+ * `{content:[{type:"text",text}],isError?}` shape. Pure value: no MCP SDK
102
+ * types leak here so the function is unit-testable with no harness.
103
+ */
104
+ export interface IdentityToolResult {
105
+ text: string;
106
+ isError?: boolean;
107
+ }
108
+ /**
109
+ * Build the `hicortex_identity` MCP tool result from the SAME pure pipeline the
110
+ * REST `GET /identity` route and the SessionStart hook use (#264 CRITICAL fix:
111
+ * previously the tool was a closure inside `createMcpServer()` that tests
112
+ * re-implemented locally, so the production path was never exercised).
113
+ *
114
+ * Pipeline (kept identical to REST + hook by CONSTRUCTION, not by mirroring):
115
+ * 1. `handleIdentityGet` — the real GET /identity handler, with the optional
116
+ * `agent` param forwarded so per-agent installs resolve the right scope
117
+ * (WARNING-2: previously the tool always passed `{}` → global, so an agent
118
+ * with an override saw the wrong identity).
119
+ * 2. `injectMemorySection` — the synthetic product-owned `memory` section
120
+ * (WARNING-1: the REST route + SessionStart hook inject it; the tool did
121
+ * not, contradicting its "same data" docs).
122
+ * 3. optional `name` filter, then `renderIdentityBlock` for the `### <Title>`
123
+ * markdown the hook injects.
124
+ *
125
+ * Pure: no I/O of its own (the only I/O is `handleIdentityGet` reading the
126
+ * identity dir, which is the same I/O the REST route does). Takes the resolved
127
+ * `identityClients` / `identityAgents` the daemon already holds at boot.
128
+ */
129
+ export declare function buildIdentityToolResult(identityDir: string, identityClients: string[], identityAgents: Record<string, AgentMode>, opts: {
130
+ name?: string;
131
+ agent?: string;
132
+ memoryInstructionsEnabled: boolean;
133
+ }): IdentityToolResult;
98
134
  export declare function gateAndRenderIdentity(data: IdentityResponse, harness: string, opts: {
99
135
  requireAgentEcho: boolean;
100
136
  }): string | null;
@@ -33,11 +33,13 @@ exports.resolveConfig = resolveConfig;
33
33
  exports.titleCaseSection = titleCaseSection;
34
34
  exports.orderSectionNames = orderSectionNames;
35
35
  exports.renderIdentityBlock = renderIdentityBlock;
36
+ exports.buildIdentityToolResult = buildIdentityToolResult;
36
37
  exports.gateAndRenderIdentity = gateAndRenderIdentity;
37
38
  exports.fetchLessonsIdentity = fetchLessonsIdentity;
38
39
  const node_fs_1 = require("node:fs");
39
40
  const node_path_1 = require("node:path");
40
41
  const identity_store_js_1 = require("./identity-store.js");
42
+ const memory_instructions_js_1 = require("./memory-instructions.js");
41
43
  const features_js_1 = require("./features.js");
42
44
  const extensions_js_1 = require("./extensions.js");
43
45
  const state_js_1 = require("./state.js");
@@ -115,7 +117,7 @@ async function fetchLessonsBlock(cfg) {
115
117
  parts.push("BEFORE making decisions, search memory: `hicortex_search` for prior decisions on the same topic.");
116
118
  parts.push("Use `hicortex_recent` at session start for recent project state.");
117
119
  if (lessonLines.length > 0) {
118
- parts.push("", "### Lessons (updated nightly)");
120
+ parts.push("", "### Learnings (updated nightly)");
119
121
  parts.push(...lessonLines);
120
122
  }
121
123
  const { index } = data;
@@ -123,16 +125,16 @@ async function fetchLessonsBlock(cfg) {
123
125
  parts.push("", "### Memory Index");
124
126
  for (const domain of moduleIndex.domains) {
125
127
  const kwStr = domain.keywords.length > 0 ? `: ${domain.keywords.join(", ")}` : "";
126
- parts.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount} lessons)${kwStr}`);
128
+ parts.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount} Learnings)${kwStr}`);
127
129
  if (domain.projects.length > 0)
128
130
  parts.push(` ${domain.projects.join(" | ")}`);
129
131
  }
130
- 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\`.`);
131
133
  }
132
134
  else if (index.projects.length > 0) {
133
135
  parts.push("", "### Memory Index");
134
136
  parts.push(index.projects.map(p => `${p.name}: ${p.count}`).join(" | "));
135
- 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\`.`);
136
138
  }
137
139
  return parts.join("\n");
138
140
  }
@@ -183,24 +185,55 @@ function renderIdentityBlock(sections) {
183
185
  return ["## Identity", "", ...bodyParts].join("\n");
184
186
  }
185
187
  /**
186
- * Gate a GET /identity response and render the `## Identity` block, or null when
187
- * nothing should be injected: `harness` not in the server-resolved `clients`,
188
- * an empty/blank section set, or when `requireAgentEcho` a response that
189
- * does not echo `agent`. The SINGLE gate used by both CC and OC so the two can
190
- * never drift (the Python Hermes plugin `provider.py::_context_block` mirrors
191
- * this logic — keep them in sync).
188
+ * Build the `hicortex_identity` MCP tool result from the SAME pure pipeline the
189
+ * REST `GET /identity` route and the SessionStart hook use (#264 CRITICAL fix:
190
+ * previously the tool was a closure inside `createMcpServer()` that tests
191
+ * re-implemented locally, so the production path was never exercised).
192
192
  *
193
- * `requireAgentEcho` is the old-server guard, and it is the CALLER's decision:
194
- * - OC passes `agentId !== null` — when it actually sent an id, a 0.12 server
195
- * that ignores `?agent=` (200 global, no echo) must NOT leak global identity
196
- * into every persona; on a bare fetch (no id) the guard is off (amendment
197
- * A2).
198
- * - CC passes `false` ALWAYS and deliberately (see the call site): a thin CC
199
- * client auto-upgrades via npx BEFORE the server does, so during the upgrade
200
- * window it talks to a 0.12 server that cannot hold ANY per-agent config —
201
- * global IS the operator's intended state there, and a guard would instead
202
- * blank ALL identity for every CC session in that window.
193
+ * Pipeline (kept identical to REST + hook by CONSTRUCTION, not by mirroring):
194
+ * 1. `handleIdentityGet` — the real GET /identity handler, with the optional
195
+ * `agent` param forwarded so per-agent installs resolve the right scope
196
+ * (WARNING-2: previously the tool always passed `{}` global, so an agent
197
+ * with an override saw the wrong identity).
198
+ * 2. `injectMemorySection` the synthetic product-owned `memory` section
199
+ * (WARNING-1: the REST route + SessionStart hook inject it; the tool did
200
+ * not, contradicting its "same data" docs).
201
+ * 3. optional `name` filter, then `renderIdentityBlock` for the `### <Title>`
202
+ * markdown the hook injects.
203
+ *
204
+ * Pure: no I/O of its own (the only I/O is `handleIdentityGet` reading the
205
+ * identity dir, which is the same I/O the REST route does). Takes the resolved
206
+ * `identityClients` / `identityAgents` the daemon already holds at boot.
203
207
  */
208
+ function buildIdentityToolResult(identityDir, identityClients, identityAgents, opts) {
209
+ // WARNING-2: forward `agent` so per-agent installs resolve the right scope.
210
+ // An invalid id makes handleIdentityGet return a 400 → surfaced as isError.
211
+ const query = opts.agent ? { agent: opts.agent } : {};
212
+ const r = (0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, query, identityAgents);
213
+ if (r.status !== 200) {
214
+ const errBody = r.body;
215
+ return {
216
+ text: `Identity fetch failed: ${JSON.stringify(errBody.error ?? r.body)}`,
217
+ isError: true,
218
+ };
219
+ }
220
+ // WARNING-1: inject the synthetic `memory` section exactly like REST + the
221
+ // SessionStart hook. `injectMemorySection` is a no-op when disabled or when
222
+ // agent mode === "off".
223
+ (0, memory_instructions_js_1.injectMemorySection)(r.body, opts.memoryInstructionsEnabled);
224
+ const sections = r.body.sections ?? {};
225
+ const filtered = opts.name
226
+ ? (sections[opts.name] !== undefined ? { [opts.name]: sections[opts.name] } : {})
227
+ : sections;
228
+ const block = renderIdentityBlock(filtered);
229
+ if (block === null) {
230
+ const text = opts.name
231
+ ? `No identity section named '${opts.name}'.`
232
+ : "No identity sections configured.";
233
+ return { text };
234
+ }
235
+ return { text: block };
236
+ }
204
237
  function gateAndRenderIdentity(data, harness, opts) {
205
238
  if (!data || typeof data !== "object")
206
239
  return null;