@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 +4 -3
- package/assets/dashboard.html +8 -3
- package/assets/viz.html +9 -3
- package/dist/consolidate.js +7 -7
- package/dist/dashboard.js +3 -3
- package/dist/db.js +24 -1
- package/dist/distiller.d.ts +5 -5
- package/dist/distiller.js +24 -14
- package/dist/eval/recall-sweep.js +2 -2
- package/dist/eval/reflection-census.js +3 -3
- package/dist/index.js +13 -13
- package/dist/learnings-identity.d.ts +36 -0
- package/dist/learnings-identity.js +53 -20
- package/dist/mcp-server.js +78 -28
- package/dist/prompts.js +12 -12
- package/dist/recall-index.js +7 -2
- package/dist/retrieval.js +2 -1
- package/dist/seed-lesson.js +1 -1
- package/dist/status.d.ts +8 -0
- package/dist/status.js +15 -1
- package/dist/storage.js +4 -4
- package/dist/type-classify.d.ts +29 -26
- package/dist/type-classify.js +52 -45
- package/dist/type-labels.d.ts +48 -17
- package/dist/type-labels.js +89 -18
- package/dist/types.d.ts +1 -1
- package/hermes-plugin/hicortex/client.py +1 -1
- package/hermes-plugin/hicortex/provider.py +13 -13
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
|
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(["
|
|
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
|
-
|
|
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(["
|
|
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
|
-
|
|
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
|
|
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: `
|
|
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}
|
|
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 /
|
|
610
|
-
|
|
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, "
|
|
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 =
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
|
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
|
-
###
|
|
125
|
-
- [
|
|
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
|
-
- [
|
|
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]
|
|
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
|
-
- [
|
|
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]
|
|
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
|
|
150
|
+
graded-schema tag model". Not knowledge (it can change) and not experience
|
|
151
151
|
(it persists and constrains).
|
|
152
|
-
- NEVER use [L] (
|
|
153
|
-
not here. If the model emits [L], it is wrong — re-tag as
|
|
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: "[
|
|
163
|
-
- NOT: "[
|
|
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
|
package/dist/recall-index.js
CHANGED
|
@@ -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
|
|
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 ?? "
|
|
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 ?? "
|
|
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 ?? "",
|
package/dist/seed-lesson.js
CHANGED
|
@@ -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: "
|
|
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 =
|
|
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 ?? "
|
|
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 ?? "
|
|
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 = '
|
|
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 = '
|
|
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);
|
package/dist/type-classify.d.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `hicortex classify-types` — deliberate, resumable
|
|
3
|
-
* reclassification pass over the memories corpus (#216).
|
|
2
|
+
* `hicortex classify-types` — deliberate, resumable experience→knowledge/
|
|
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 "
|
|
9
|
-
* corpus was ~98%
|
|
10
|
-
* time via the [E]/[
|
|
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
|
-
*
|
|
18
|
-
* regardless of current type **except
|
|
19
|
-
* ONE constrained LLM call asks the model to classify the content as
|
|
20
|
-
*
|
|
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
|
-
*
|
|
27
|
-
* scope explicitly excludes `memory_type = '
|
|
28
|
-
* defence. (The prompt asks only for
|
|
29
|
-
* DID enter scope would be overwritten
|
|
30
|
-
* "
|
|
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
|
|
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
|
-
/**
|
|
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 (
|
|
74
|
-
* distinction mirrors the distiller's [E]/[
|
|
75
|
-
* so distill-time and backfill-time classification stay
|
|
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. "
|
|
82
|
-
* (the reflection stage owns
|
|
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: "
|
|
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: "
|
|
101
|
+
type: "experience" | "knowledge" | "decisions";
|
|
99
102
|
score: number;
|
|
100
103
|
} | null>;
|
|
101
104
|
/**
|