@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.
@@ -73,6 +73,7 @@ const recall_index_js_1 = require("./recall-index.js");
73
73
  const type_labels_js_1 = require("./type-labels.js");
74
74
  const health_js_1 = require("./health.js");
75
75
  const seed_lesson_js_1 = require("./seed-lesson.js");
76
+ const learnings_identity_js_1 = require("./learnings-identity.js");
76
77
  const distiller_js_1 = require("./distiller.js");
77
78
  const dedup_js_1 = require("./dedup.js");
78
79
  const redact_js_1 = require("./redact.js");
@@ -182,10 +183,10 @@ function createMcpServer() {
182
183
  }
183
184
  });
184
185
  // -- hicortex_ingest --
185
- 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.", {
186
187
  content: zod_1.z.string().describe("Memory content to store"),
187
188
  project: zod_1.z.string().optional().describe("Project this memory belongs to"),
188
- 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)."),
189
190
  }, async ({ content, project, memory_type }) => {
190
191
  if (!db)
191
192
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
@@ -194,7 +195,8 @@ function createMcpServer() {
194
195
  const id = storage.insertMemory(db, content, embedding, {
195
196
  sourceAgent: "claude-code/manual",
196
197
  project,
197
- 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",
198
200
  });
199
201
  return { content: [{ type: "text", text: `Memory stored (id: ${id.slice(0, 8)})` }] };
200
202
  }
@@ -207,7 +209,7 @@ function createMcpServer() {
207
209
  id: zod_1.z.string().describe("Memory ID (from search results, first 8 chars or full UUID)"),
208
210
  content: zod_1.z.string().optional().describe("New content text"),
209
211
  project: zod_1.z.string().optional().describe("New project name"),
210
- 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)."),
211
213
  }, async ({ id, content, project, memory_type }) => {
212
214
  if (!db)
213
215
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
@@ -221,8 +223,9 @@ function createMcpServer() {
221
223
  fields.content = content;
222
224
  if (project !== undefined)
223
225
  fields.project = project;
226
+ // Normalize legacy raw enum to canonical human terms before DB write.
224
227
  if (memory_type !== undefined)
225
- fields.memory_type = memory_type;
228
+ fields.memory_type = (0, type_labels_js_1.normalizeMemoryType)(memory_type);
226
229
  if (Object.keys(fields).length === 0) {
227
230
  return { content: [{ type: "text", text: "No fields to update" }], isError: true };
228
231
  }
@@ -260,23 +263,56 @@ function createMcpServer() {
260
263
  return { content: [{ type: "text", text: `Delete failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
261
264
  }
262
265
  });
263
- // -- hicortex_lessons --
264
- server.tool("hicortex_lessons", "Get actionable lessons learned from past sessions. Auto-generated insights about mistakes to avoid.", {
265
- days: zod_1.z.coerce.number().optional().describe("Look back N days (default 7)"),
266
- project: zod_1.z.string().optional().describe("Filter by project name"),
267
- }, async ({ days, project }) => {
266
+ // -- hicortex_learnings (canonical) + hicortex_lessons (alias) --
267
+ const learningsHandler = async ({ days, project }) => {
268
268
  if (!db)
269
269
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
270
270
  try {
271
271
  const lessons = storage.getLessons(db, days ?? 7, project);
272
272
  if (lessons.length === 0) {
273
- return { content: [{ type: "text", text: "No lessons found for the specified period." }] };
273
+ return { content: [{ type: "text", text: "No Learnings found for the specified period." }] };
274
274
  }
275
275
  const text = lessons.map((l) => `- ${l.content.slice(0, 500)}`).join("\n");
276
276
  return { content: [{ type: "text", text }] };
277
277
  }
278
278
  catch (err) {
279
- return { content: [{ type: "text", text: `Lessons fetch failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
279
+ return { content: [{ type: "text", text: `Learnings fetch failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
280
+ }
281
+ };
282
+ const learningsSchema = {
283
+ days: zod_1.z.coerce.number().optional().describe("Look back N days (default 7)"),
284
+ project: zod_1.z.string().optional().describe("Filter by project name"),
285
+ };
286
+ server.tool("hicortex_learnings", "Get actionable Learnings from past sessions. Auto-generated insights about mistakes to avoid.", learningsSchema, learningsHandler);
287
+ server.tool("hicortex_lessons", "Get actionable Learnings from past sessions. (Alias for hicortex_learnings.)", learningsSchema, learningsHandler);
288
+ // -- hicortex_identity --
289
+ // Standing identity layer on-demand (the same data GET /identity returns and
290
+ // the SessionStart hook injects). Lets an agent re-read its identity after
291
+ // context compaction, or look up one named section, mid-session. Renders the
292
+ // same `### <Title>` section markdown the hook injects (shared pipeline in
293
+ // learnings-identity.ts → buildIdentityToolResult) so the agent sees one
294
+ // consistent shape. The handler is a thin wrapper over that pure function;
295
+ // tests exercise it directly (no MCP SDK plumbing re-implemented).
296
+ server.tool("hicortex_identity", "Fetch your standing identity — the hand-edited 'who you are + how you work' layer (personality, rules, preferences). Returns all sections or a specific one. Use this to re-read your identity after context compaction or to look up a specific rule. On multi-agent installs, pass `agent` to fetch a specific agent's scoped identity; omit for the global identity.", {
297
+ name: zod_1.z.string().optional().describe("Fetch a specific identity section by name (e.g. 'rules'). Omit for all sections."),
298
+ agent: zod_1.z.string().optional().describe("Fetch a specific agent's identity scope (for per-agent installs). Omit for global."),
299
+ }, async ({ name, agent }) => {
300
+ if (!db)
301
+ return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
302
+ try {
303
+ const identityDir = (0, node_path_1.join)(stateDir, "identity");
304
+ // Single pipeline shared with REST /identity + the SessionStart hook
305
+ // (#264 CRITICAL + WARNING-1 + WARNING-2). The pure function owns
306
+ // handleIdentityGet → injectMemorySection → renderIdentityBlock.
307
+ const result = (0, learnings_identity_js_1.buildIdentityToolResult)(identityDir, identityClients, identityAgents, {
308
+ name,
309
+ agent,
310
+ memoryInstructionsEnabled,
311
+ });
312
+ return { content: [{ type: "text", text: result.text }], isError: result.isError };
313
+ }
314
+ catch (err) {
315
+ return { content: [{ type: "text", text: `Identity fetch failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
280
316
  }
281
317
  });
282
318
  // -- hicortex_index --
@@ -285,7 +321,7 @@ function createMcpServer() {
285
321
  const moduleIndex = state.moduleIndex;
286
322
  if (moduleIndex && moduleIndex.domains.length > 0) {
287
323
  const text = moduleIndex.domains.map((d) => {
288
- const head = `**${d.name}** (${d.memoryCount} memories, ${d.lessonCount} lessons)`;
324
+ const head = `**${d.name}** (${d.memoryCount} memories, ${d.lessonCount} Learnings)`;
289
325
  // Content-based domains carry a description and no projects; legacy
290
326
  // project-grouping domains carry a project list + keywords.
291
327
  if (d.description && d.projects.length === 0) {
@@ -606,8 +642,11 @@ async function startServer(options = {}) {
606
642
  llmLabel: llmConfig ? `${llmConfig.provider}/${llmConfig.model}` : "not configured",
607
643
  }));
608
644
  });
609
- // REST /lessons return lessons + memory index for client CLAUDE.md injection
610
- app.get("/lessons", (_req, res) => {
645
+ // REST /learnings (canonical, #264) + /lessons (alias) return lessons +
646
+ // memory index for client CLAUDE.md injection. Both routes share ONE handler
647
+ // so the alias can never drift from the canonical shape. The legacy name is
648
+ // kept indefinitely (existing SessionStart hooks literally fetch /lessons).
649
+ const learningsIndexHandler = (_req, res) => {
611
650
  if (!db) {
612
651
  res.status(503).json({ error: "Server not initialized" });
613
652
  return;
@@ -639,9 +678,11 @@ async function startServer(options = {}) {
639
678
  });
640
679
  }
641
680
  catch (err) {
642
- (0, health_js_1.logAndSendInternalError)(res, "lessons", err);
681
+ (0, health_js_1.logAndSendInternalError)(res, "learnings", err);
643
682
  }
644
- });
683
+ };
684
+ app.get("/learnings", learningsIndexHandler);
685
+ app.get("/lessons", learningsIndexHandler); // #264 backcompat alias
645
686
  // REST /ingest — accept pre-distilled memories from remote clients
646
687
  app.post("/ingest", async (req, res) => {
647
688
  if (!db) {
@@ -653,11 +694,15 @@ async function startServer(options = {}) {
653
694
  res.status(400).json({ error: "Missing or invalid 'content' field" });
654
695
  return;
655
696
  }
656
- const validTypes = ["episode", "lesson", "fact", "decision"];
697
+ const validTypes = type_labels_js_1.ACCEPTED_MEMORY_TYPES;
657
698
  if (memory_type && !validTypes.includes(memory_type)) {
658
699
  res.status(400).json({ error: `Invalid memory_type: ${memory_type}` });
659
700
  return;
660
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;
661
706
  // Dedup by source_session (idempotent — skip if already ingested)
662
707
  if (source_session) {
663
708
  const existing = db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE source_session = ?").get(source_session);
@@ -675,7 +720,7 @@ async function startServer(options = {}) {
675
720
  sourceDomain: typeof source_domain === "string" ? source_domain : null,
676
721
  sourceSession: source_session ?? undefined,
677
722
  project: project ?? undefined,
678
- memoryType: memory_type ?? "episode",
723
+ memoryType: normalizedType ?? "experience",
679
724
  // 0.16.x: privacy defaults to null (vestigial column). A legacy client
680
725
  // that sends an explicit value is honored; absent → null.
681
726
  privacy: typeof privacy === "string" ? privacy : null,
@@ -1022,9 +1067,9 @@ async function startServer(options = {}) {
1022
1067
  sourceSession: sourcePrefix ? `${sourcePrefix}#${i}` : undefined,
1023
1068
  project: project ?? undefined,
1024
1069
  // #216: the distiller now classifies each entry as
1025
- // episode/fact/decision via the [E]/[F]/[D] tag parsed in
1070
+ // experience/knowledge/decisions via the [E]/[K]/[D] tag parsed in
1026
1071
  // distiller.ts. Pre-#216 distiller output (no tag) defaults to
1027
- // episode in the parser, so this is backward compatible.
1072
+ // experience in the parser, so this is backward compatible.
1028
1073
  memoryType,
1029
1074
  // 0.16.x: privacy defaults to null (vestigial column). A legacy
1030
1075
  // client that sends an explicit value is honored; absent → null.
@@ -1072,19 +1117,24 @@ async function startServer(options = {}) {
1072
1117
  fields.content = content;
1073
1118
  if (project !== undefined)
1074
1119
  fields.project = project;
1075
- if (memory_type !== undefined)
1076
- fields.memory_type = memory_type;
1077
1120
  if (privacy !== undefined)
1078
1121
  fields.privacy = privacy;
1079
- if (Object.keys(fields).length === 0) {
1080
- res.status(400).json({ error: "No fields to update" });
1081
- return;
1082
- }
1083
- 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;
1084
1125
  if (memory_type !== undefined && !validTypes.includes(memory_type)) {
1085
1126
  res.status(400).json({ error: `Invalid memory_type: ${memory_type}` });
1086
1127
  return;
1087
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
+ }
1088
1138
  try {
1089
1139
  storage.updateMemory(db, fullId, fields);
1090
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
@@ -255,10 +255,15 @@ function handleMemoryGet(db, query) {
255
255
  // `citation` is server-rendered so every plugin surfaces the same built-in
256
256
  // provenance norm (owner directive 27.07) — see #193.
257
257
  const date = (mem.created_at ?? "").slice(0, 10);
258
+ // Shallow-copy and apply the human-term label to memory_type so the REST
259
+ // response surfaces the user-facing vocabulary, not the raw DB enum. The
260
+ // underlying DB row (`mem`) is NOT mutated — the DB IS the raw-enum source
261
+ // of truth; the label is a presentation concern applied at the boundary.
262
+ const memory = { ...mem, memory_type: (0, type_labels_js_1.labelForType)(mem.memory_type) };
258
263
  return {
259
264
  status: 200,
260
265
  body: {
261
- memory: mem,
266
+ memory,
262
267
  citation: `(memory ${String(mem.id).slice(0, 8)}, ${date}, from ${mem.source_agent ?? "unknown"}, FETCHED)`,
263
268
  },
264
269
  };
@@ -284,7 +289,7 @@ function formatMemoryGetText(db, query) {
284
289
  const date = (mem.created_at ?? "").slice(0, 10);
285
290
  // #264 WS2: render the human-term label (Knowledge/Experience/...), not the
286
291
  // internal enum, in the citation header shown to the agent/user.
287
- const header = `[memory ${mem.id} | ${(0, type_labels_js_1.labelForType)(mem.memory_type ?? "episode")} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
292
+ const header = `[memory ${mem.id} | ${(0, type_labels_js_1.labelForType)(mem.memory_type ?? "experience")} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
288
293
  `Cite as ${citation} where this shapes your answer; it may be stale — newer memories supersede older.`;
289
294
  return { status: 200, text: `${header}\n\n${mem.content ?? ""}` };
290
295
  }
package/dist/retrieval.js CHANGED
@@ -69,6 +69,7 @@ exports.retrieve = retrieve;
69
69
  exports.searchRecent = searchRecent;
70
70
  const storage = __importStar(require("./storage.js"));
71
71
  const schema_prototypes_js_1 = require("./schema-prototypes.js");
72
+ const type_labels_js_1 = require("./type-labels.js");
72
73
  /** Default decay half-life (days) at importance 0.5. #192: was 0.0005/h
73
74
  * (~115-day half-life at base 0.5) — aggressive enough to bury the long tail
74
75
  * in ranking. Long-term remembering is the product; time preference stays,
@@ -450,7 +451,7 @@ function formatResult(memory, score, effStr, connections, provenance) {
450
451
  score: Math.round(score * 1e6) / 1e6,
451
452
  effective_strength: Math.round(effStr * 1e6) / 1e6,
452
453
  access_count: memory.access_count ?? 0,
453
- memory_type: memory.memory_type ?? "episode",
454
+ memory_type: (0, type_labels_js_1.labelForType)(memory.memory_type ?? "experience"),
454
455
  project: memory.project ?? null,
455
456
  source_agent: memory.source_agent ?? null,
456
457
  created_at: memory.created_at ?? "",
@@ -63,7 +63,7 @@ async function injectSeedLesson(database, log = console.log) {
63
63
  storage.insertMemory(database, exports.SEED_LESSON, embedding, {
64
64
  sourceAgent: "hicortex/seed",
65
65
  project: "global",
66
- memoryType: "lesson",
66
+ memoryType: "learnings",
67
67
  baseStrength: 0.95,
68
68
  });
69
69
  log("[hicortex] Seed lesson injected: Daily Self-Improvement Protocol");
package/dist/status.d.ts CHANGED
@@ -10,4 +10,12 @@
10
10
  * out as invalid (the hook sends none) rather than silently accepted.
11
11
  */
12
12
  export declare function statusAgentLine(config: Record<string, unknown>): string;
13
+ /**
14
+ * Format the memory-type breakdown for `hicortex status`. Each raw DB enum key
15
+ * is rendered through {@link labelForType} so the printed line uses the human
16
+ * vocabulary (Knowledge/Experience/Decisions/Learnings), not the raw enum.
17
+ * Extracted from `runStatus` so the labeling is unit-testable without booting
18
+ * the full status printer. Unknown keys pass through verbatim (forward-compat).
19
+ */
20
+ export declare function formatTypeBreakdown(byType: Record<string, number>): string;
13
21
  export declare function runStatus(): Promise<void>;
package/dist/status.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.statusAgentLine = statusAgentLine;
7
+ exports.formatTypeBreakdown = formatTypeBreakdown;
7
8
  exports.runStatus = runStatus;
8
9
  const paths_js_1 = require("./paths.js");
9
10
  const node_fs_1 = require("node:fs");
@@ -14,6 +15,7 @@ const db_js_1 = require("./db.js");
14
15
  const features_js_1 = require("./features.js");
15
16
  const state_js_1 = require("./state.js");
16
17
  const identity_store_js_1 = require("./identity-store.js");
18
+ const type_labels_js_1 = require("./type-labels.js");
17
19
  const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
18
20
  const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
19
21
  const OC_CONFIG = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
@@ -36,6 +38,18 @@ function statusAgentLine(config) {
36
38
  return "(not set — global identity)";
37
39
  }
38
40
  }
41
+ /**
42
+ * Format the memory-type breakdown for `hicortex status`. Each raw DB enum key
43
+ * is rendered through {@link labelForType} so the printed line uses the human
44
+ * vocabulary (Knowledge/Experience/Decisions/Learnings), not the raw enum.
45
+ * Extracted from `runStatus` so the labeling is unit-testable without booting
46
+ * the full status printer. Unknown keys pass through verbatim (forward-compat).
47
+ */
48
+ function formatTypeBreakdown(byType) {
49
+ return Object.entries(byType)
50
+ .map(([k, v]) => `${(0, type_labels_js_1.labelForType)(k)}=${v}`)
51
+ .join(", ");
52
+ }
39
53
  async function runStatus() {
40
54
  console.log("Hicortex Status");
41
55
  console.log("─".repeat(40));
@@ -48,7 +62,7 @@ async function runStatus() {
48
62
  const { initDb, getStats } = await import("./db.js");
49
63
  const db = initDb(dbPath);
50
64
  const stats = getStats(db, dbPath);
51
- const typeStr = Object.entries(stats.by_type).map(([k, v]) => `${k}=${v}`).join(", ");
65
+ const typeStr = formatTypeBreakdown(stats.by_type);
52
66
  console.log(`Memories: ${stats.memories} (${typeStr || "none"})`);
53
67
  console.log(`Links: ${stats.links}`);
54
68
  console.log(`DB size: ${(stats.db_size_bytes / 1024).toFixed(1)} KB`);
package/dist/storage.js CHANGED
@@ -72,7 +72,7 @@ function insertMemory(db, content, embedding, opts = {}) {
72
72
  created_at, ingested_at, source_agent, source_agent_id, source_session,
73
73
  source_domain, project, privacy, memory_type)
74
74
  VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
75
- .run(id, content, opts.baseStrength ?? 0.5, ts, ts, ingestedTs, opts.sourceAgent ?? "default", opts.sourceAgentId ?? null, sourceSession, opts.sourceDomain ?? null, opts.project ?? null, opts.privacy ?? null, opts.memoryType ?? "episode");
75
+ .run(id, content, opts.baseStrength ?? 0.5, ts, ts, ingestedTs, opts.sourceAgent ?? "default", opts.sourceAgentId ?? null, sourceSession, opts.sourceDomain ?? null, opts.project ?? null, opts.privacy ?? null, opts.memoryType ?? "experience");
76
76
  if (result.changes > 0) {
77
77
  // New row — store its vector.
78
78
  db.prepare("INSERT INTO memory_vectors (id, embedding) VALUES (?, ?)").run(id, embedToBlob(embedding));
@@ -469,7 +469,7 @@ function insertMemoriesBatch(db, memories) {
469
469
  for (const mem of memories) {
470
470
  const id = (0, node_crypto_1.randomUUID)();
471
471
  const ts = nowIso();
472
- insertMem.run(id, mem.content, mem.baseStrength ?? 0.5, ts, ts, ts, mem.sourceAgent ?? "default", mem.sourceAgentId ?? null, mem.sourceSession ?? null, mem.sourceDomain ?? null, mem.project ?? null, mem.privacy ?? null, mem.memoryType ?? "episode");
472
+ insertMem.run(id, mem.content, mem.baseStrength ?? 0.5, ts, ts, ts, mem.sourceAgent ?? "default", mem.sourceAgentId ?? null, mem.sourceSession ?? null, mem.sourceDomain ?? null, mem.project ?? null, mem.privacy ?? null, mem.memoryType ?? "experience");
473
473
  insertVec.run(id, embedToBlob(mem.embedding));
474
474
  count++;
475
475
  }
@@ -514,14 +514,14 @@ function getLessons(db, days = 7, project) {
514
514
  if (project) {
515
515
  const rows = db
516
516
  .prepare(`SELECT * FROM memories
517
- WHERE memory_type = 'lesson' AND created_at > ? AND project = ?
517
+ WHERE memory_type = 'learnings' AND created_at > ? AND project = ?
518
518
  ORDER BY created_at DESC`)
519
519
  .all(cutoff, project);
520
520
  return rows.map(rowToMemory);
521
521
  }
522
522
  const rows = db
523
523
  .prepare(`SELECT * FROM memories
524
- WHERE memory_type = 'lesson' AND created_at > ?
524
+ WHERE memory_type = 'learnings' AND created_at > ?
525
525
  ORDER BY created_at DESC`)
526
526
  .all(cutoff);
527
527
  return rows.map(rowToMemory);
@@ -1,41 +1,41 @@
1
1
  /**
2
- * `hicortex classify-types` — deliberate, resumable episodefact/decision
3
- * reclassification pass over the memories corpus (#216).
2
+ * `hicortex classify-types` — deliberate, resumable experienceknowledge/
3
+ * decisions reclassification pass over the memories corpus (#216).
4
4
  *
5
5
  * WHY THIS EXISTS
6
6
  * ---------------
7
7
  * Before #216 the distiller NEVER set memory_type — every distilled memory
8
- * defaulted to "episode" (storage.ts insertMemory `?? "episode"`), so the
9
- * corpus was ~98% episodes. The distiller now classifies each entry at extract
10
- * time via the [E]/[F]/[D] tag (distiller.ts), but the EXISTING corpus needs a
11
- * one-shot backfill. This command is that backfill — modelled on
8
+ * defaulted to "experience" (storage.ts insertMemory `?? "experience"`), so
9
+ * the corpus was ~98% experiences. The distiller now classifies each entry at
10
+ * extract time via the [E]/[K]/[D] tag (distiller.ts), but the EXISTING corpus
11
+ * needs a one-shot backfill. This command is that backfill — modelled on
12
12
  * `classify-domains` (resumable cursor, batched, infra-error-safe).
13
13
  *
14
14
  * WHAT IT DOES
15
15
  * ------------
16
16
  * Walks memories ordered by rowid in batches (default 200). Default scope =
17
- * episodes only (`memory_type = 'episode'`); `--all` reclassifies every memory
18
- * regardless of current type **except lessons** (see below). For each memory,
19
- * ONE constrained LLM call asks the model to classify the content as
20
- * episode / fact / decision. The reply is parsed + validated, and
17
+ * experiences only (`memory_type = 'experience'`); `--all` reclassifies every
18
+ * memory regardless of current type **except learnings** (see below). For each
19
+ * memory, ONE constrained LLM call asks the model to classify the content as
20
+ * experience / knowledge / decisions. The reply is parsed + validated, and
21
21
  * `UPDATE memories SET memory_type = ? WHERE id = ?` runs inside a per-batch
22
22
  * transaction. The cursor (`typeCursor` in state.json) advances to the last
23
23
  * committed rowid after each batch — crash-safe and infra-abort-safe (same
24
24
  * discipline as classify-domains).
25
25
  *
26
- * Lessons are NEVER touched here: the reflection stage owns them. The `--all`
27
- * scope explicitly excludes `memory_type = 'lesson'` — this is the primary
28
- * defence. (The prompt asks only for episode/fact/decision, so a lesson that
29
- * DID enter scope would be overwritten to E/F/D — the model never replies
30
- * "lesson". `parseTypeReply`'s rejection of a "lesson" reply is a backstop, not
31
- * the main guard.)
26
+ * Learnings are NEVER touched here: the reflection stage owns them. The
27
+ * `--all` scope explicitly excludes `memory_type = 'learnings'` — this is the
28
+ * primary defence. (The prompt asks only for experience/knowledge/decisions,
29
+ * so a learning that DID enter scope would be overwritten — the model never
30
+ * replies "learnings". `parseTypeReply`'s rejection of a "learnings" reply is
31
+ * a backstop, not the main guard.)
32
32
  *
33
33
  * This command does NOT use the consolidation budget — it is a standalone CLI,
34
34
  * not a nightly stage.
35
35
  */
36
36
  import { LlmClient } from "./llm.js";
37
37
  export interface ClassifyTypesOptions {
38
- /** Reclassify EVERY memory, not just episodes. */
38
+ /** Reclassify EVERY memory, not just experiences. */
39
39
  all?: boolean;
40
40
  /** Memories per batch (default 200). Cursor advances per committed batch. */
41
41
  batchSize?: number;
@@ -55,7 +55,7 @@ export interface ClassifyTypesReport {
55
55
  scanned: number;
56
56
  /** Memories whose memory_type was changed. */
57
57
  reclassified: number;
58
- /** Episodes confirmed as episode (no change). */
58
+ /** Experiences confirmed as experience (no change). */
59
59
  unchanged: number;
60
60
  /** Memories skipped due to an infra error (LLM threw twice). */
61
61
  failed: number;
@@ -70,23 +70,26 @@ export interface ClassifyTypesReport {
70
70
  }
71
71
  /**
72
72
  * Build the constrained type-classification prompt for one memory. The model
73
- * must reply with ONLY the type word (episode/fact/decision) — no prose. The
74
- * distinction mirrors the distiller's [E]/[F]/[D] tag definitions (prompts.ts),
75
- * so distill-time and backfill-time classification stay consistent.
73
+ * must reply with ONLY the type word (experience/knowledge/decisions) — no
74
+ * prose. The distinction mirrors the distiller's [E]/[K]/[D] tag definitions
75
+ * (prompts.ts), so distill-time and backfill-time classification stay
76
+ * consistent. (The stored enum was renamed in #264: episode→experience,
77
+ * fact→knowledge, decision→decisions; the conceptual definitions are
78
+ * unchanged.)
76
79
  */
77
80
  export declare function buildTypeClassifyPrompt(content: string): string;
78
81
  /**
79
82
  * Parse the model's reply into a validated type. Accepts the bare word
80
83
  * (case-insensitive), tolerating surrounding whitespace, a trailing period, a
81
- * leading "Type:" label, and markdown emphasis. "lesson" is NEVER accepted
82
- * (the reflection stage owns lessons; a model that emits it is wrong) — returns
83
- * null so the caller retries.
84
+ * leading "Type:" label, and markdown emphasis. "learnings" (and the legacy
85
+ * "lesson") are NEVER accepted (the reflection stage owns learnings; a model
86
+ * that emits either is wrong) — returns null so the caller retries.
84
87
  *
85
88
  * Returns null on anything unparseable or out-of-vocabulary so the caller can
86
89
  * retry once (matching classify-domains' two-attempt discipline).
87
90
  */
88
91
  export declare function parseTypeReply(reply: string): {
89
- type: "episode" | "fact" | "decision";
92
+ type: "experience" | "knowledge" | "decisions";
90
93
  score: number;
91
94
  } | null;
92
95
  /**
@@ -95,7 +98,7 @@ export declare function parseTypeReply(reply: string): {
95
98
  * (caller leaves the memory untouched and retries via the cursor next run).
96
99
  */
97
100
  export declare function classifyMemoryType(content: string, llm: LlmClient): Promise<{
98
- type: "episode" | "fact" | "decision";
101
+ type: "experience" | "knowledge" | "decisions";
99
102
  score: number;
100
103
  } | null>;
101
104
  /**