@gamaze/hicortex 0.18.1 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.js +4 -4
- package/dist/mcp-server.js +28 -17
- 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/assets/dashboard.html
CHANGED
|
@@ -238,10 +238,15 @@ let compKey = "by_type";
|
|
|
238
238
|
|
|
239
239
|
const $ = (id) => document.getElementById(id);
|
|
240
240
|
|
|
241
|
-
// #264
|
|
242
|
-
//
|
|
243
|
-
//
|
|
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
|
|
393
|
-
//
|
|
394
|
-
//
|
|
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",
|
package/dist/consolidate.js
CHANGED
|
@@ -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='
|
|
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 === "
|
|
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: "
|
|
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 = '
|
|
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 = '
|
|
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 === "
|
|
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 = '
|
|
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 = '
|
|
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 === "
|
|
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 = '
|
|
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 '
|
|
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.
|
package/dist/distiller.d.ts
CHANGED
|
@@ -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 "
|
|
68
|
-
* "
|
|
69
|
-
* absent:
|
|
70
|
-
* (#216). A missing/unknown tag defaults to "
|
|
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: "
|
|
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 [
|
|
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]/[
|
|
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
|
-
*
|
|
430
|
-
* distiller must NEVER emit
|
|
431
|
-
* model that emits `[L]` is wrong and we do not propagate
|
|
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 "
|
|
443
|
+
case "K":
|
|
444
|
+
case "k":
|
|
445
|
+
case "F": // legacy tag (was Fact)
|
|
436
446
|
case "f":
|
|
437
|
-
return "
|
|
447
|
+
return "knowledge";
|
|
438
448
|
case "D":
|
|
439
449
|
case "d":
|
|
440
|
-
return "
|
|
441
|
-
// E, e, L, l (rejected), undefined, or anything else →
|
|
450
|
+
return "decisions";
|
|
451
|
+
// E, e, L, l (rejected), undefined, or anything else → experience.
|
|
442
452
|
default:
|
|
443
|
-
return "
|
|
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 "
|
|
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(/^\[([
|
|
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 →
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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 = '
|
|
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 = '
|
|
18
|
-
const totalEpisodes = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = '
|
|
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
|
|
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
|
|
181
|
-
`These are actionable
|
|
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
|
|
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
|
|
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: ["
|
|
493
|
-
description: "Type of memory (default:
|
|
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
|
|
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
|
|
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
|
|
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: ["
|
|
616
|
-
description: "New memory type",
|
|
615
|
+
enum: ["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"],
|
|
616
|
+
description: "New memory type. Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term).",
|
|
617
617
|
},
|
|
618
618
|
},
|
|
619
619
|
required: ["id"],
|
|
@@ -624,7 +624,7 @@ exports.default = {
|
|
|
624
624
|
id: args.id,
|
|
625
625
|
content: args.content,
|
|
626
626
|
project: args.project,
|
|
627
|
-
memory_type: args.memory_type,
|
|
627
|
+
memory_type: args.memory_type ? (0, type_labels_js_1.normalizeMemoryType)(args.memory_type) : undefined,
|
|
628
628
|
}, 15000);
|
|
629
629
|
if (result.status === 404) {
|
|
630
630
|
return { error: `Memory not found: ${args.id}` };
|
|
@@ -117,7 +117,7 @@ async function fetchLessonsBlock(cfg) {
|
|
|
117
117
|
parts.push("BEFORE making decisions, search memory: `hicortex_search` for prior decisions on the same topic.");
|
|
118
118
|
parts.push("Use `hicortex_recent` at session start for recent project state.");
|
|
119
119
|
if (lessonLines.length > 0) {
|
|
120
|
-
parts.push("", "###
|
|
120
|
+
parts.push("", "### Learnings (updated nightly)");
|
|
121
121
|
parts.push(...lessonLines);
|
|
122
122
|
}
|
|
123
123
|
const { index } = data;
|
|
@@ -125,16 +125,16 @@ async function fetchLessonsBlock(cfg) {
|
|
|
125
125
|
parts.push("", "### Memory Index");
|
|
126
126
|
for (const domain of moduleIndex.domains) {
|
|
127
127
|
const kwStr = domain.keywords.length > 0 ? `: ${domain.keywords.join(", ")}` : "";
|
|
128
|
-
parts.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount}
|
|
128
|
+
parts.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount} Learnings)${kwStr}`);
|
|
129
129
|
if (domain.projects.length > 0)
|
|
130
130
|
parts.push(` ${domain.projects.join(" | ")}`);
|
|
131
131
|
}
|
|
132
|
-
parts.push(`${index.total} memories, ${index.lessonCount}
|
|
132
|
+
parts.push(`${index.total} memories, ${index.lessonCount} Learnings, ${index.sourceCount} agents. Search with \`hicortex_search\`.`);
|
|
133
133
|
}
|
|
134
134
|
else if (index.projects.length > 0) {
|
|
135
135
|
parts.push("", "### Memory Index");
|
|
136
136
|
parts.push(index.projects.map(p => `${p.name}: ${p.count}`).join(" | "));
|
|
137
|
-
parts.push(`${index.total} memories, ${index.lessonCount}
|
|
137
|
+
parts.push(`${index.total} memories, ${index.lessonCount} Learnings, ${index.sourceCount} agents. Search with \`hicortex_search\`.`);
|
|
138
138
|
}
|
|
139
139
|
return parts.join("\n");
|
|
140
140
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -183,10 +183,10 @@ function createMcpServer() {
|
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
// -- hicortex_ingest --
|
|
186
|
-
server.tool("hicortex_ingest", "Store a new memory in long-term storage. Use for
|
|
186
|
+
server.tool("hicortex_ingest", "Store a new memory in long-term storage. Use for Knowledge, Decisions, or Learnings.", {
|
|
187
187
|
content: zod_1.z.string().describe("Memory content to store"),
|
|
188
188
|
project: zod_1.z.string().optional().describe("Project this memory belongs to"),
|
|
189
|
-
memory_type: zod_1.z.enum(["
|
|
189
|
+
memory_type: zod_1.z.enum(["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"]).optional().describe("Type of memory (default: Experience). Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term)."),
|
|
190
190
|
}, async ({ content, project, memory_type }) => {
|
|
191
191
|
if (!db)
|
|
192
192
|
return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
|
|
@@ -195,7 +195,8 @@ function createMcpServer() {
|
|
|
195
195
|
const id = storage.insertMemory(db, content, embedding, {
|
|
196
196
|
sourceAgent: "claude-code/manual",
|
|
197
197
|
project,
|
|
198
|
-
|
|
198
|
+
// Normalize legacy raw enum to the canonical term the DB stores.
|
|
199
|
+
memoryType: memory_type ? (0, type_labels_js_1.normalizeMemoryType)(memory_type) : "experience",
|
|
199
200
|
});
|
|
200
201
|
return { content: [{ type: "text", text: `Memory stored (id: ${id.slice(0, 8)})` }] };
|
|
201
202
|
}
|
|
@@ -208,7 +209,7 @@ function createMcpServer() {
|
|
|
208
209
|
id: zod_1.z.string().describe("Memory ID (from search results, first 8 chars or full UUID)"),
|
|
209
210
|
content: zod_1.z.string().optional().describe("New content text"),
|
|
210
211
|
project: zod_1.z.string().optional().describe("New project name"),
|
|
211
|
-
memory_type: zod_1.z.enum(["
|
|
212
|
+
memory_type: zod_1.z.enum(["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"]).optional().describe("New memory type. Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term)."),
|
|
212
213
|
}, async ({ id, content, project, memory_type }) => {
|
|
213
214
|
if (!db)
|
|
214
215
|
return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
|
|
@@ -222,8 +223,9 @@ function createMcpServer() {
|
|
|
222
223
|
fields.content = content;
|
|
223
224
|
if (project !== undefined)
|
|
224
225
|
fields.project = project;
|
|
226
|
+
// Normalize legacy raw enum to canonical human terms before DB write.
|
|
225
227
|
if (memory_type !== undefined)
|
|
226
|
-
fields.memory_type = memory_type;
|
|
228
|
+
fields.memory_type = (0, type_labels_js_1.normalizeMemoryType)(memory_type);
|
|
227
229
|
if (Object.keys(fields).length === 0) {
|
|
228
230
|
return { content: [{ type: "text", text: "No fields to update" }], isError: true };
|
|
229
231
|
}
|
|
@@ -319,7 +321,7 @@ function createMcpServer() {
|
|
|
319
321
|
const moduleIndex = state.moduleIndex;
|
|
320
322
|
if (moduleIndex && moduleIndex.domains.length > 0) {
|
|
321
323
|
const text = moduleIndex.domains.map((d) => {
|
|
322
|
-
const head = `**${d.name}** (${d.memoryCount} memories, ${d.lessonCount}
|
|
324
|
+
const head = `**${d.name}** (${d.memoryCount} memories, ${d.lessonCount} Learnings)`;
|
|
323
325
|
// Content-based domains carry a description and no projects; legacy
|
|
324
326
|
// project-grouping domains carry a project list + keywords.
|
|
325
327
|
if (d.description && d.projects.length === 0) {
|
|
@@ -692,11 +694,15 @@ async function startServer(options = {}) {
|
|
|
692
694
|
res.status(400).json({ error: "Missing or invalid 'content' field" });
|
|
693
695
|
return;
|
|
694
696
|
}
|
|
695
|
-
const validTypes =
|
|
697
|
+
const validTypes = type_labels_js_1.ACCEPTED_MEMORY_TYPES;
|
|
696
698
|
if (memory_type && !validTypes.includes(memory_type)) {
|
|
697
699
|
res.status(400).json({ error: `Invalid memory_type: ${memory_type}` });
|
|
698
700
|
return;
|
|
699
701
|
}
|
|
702
|
+
// Normalize legacy raw enum (fact/episode/decision/lesson) to the
|
|
703
|
+
// canonical term the DB stores (knowledge/experience/decisions/learnings).
|
|
704
|
+
// Canonical values pass through unchanged.
|
|
705
|
+
const normalizedType = memory_type ? (0, type_labels_js_1.normalizeMemoryType)(memory_type) : memory_type;
|
|
700
706
|
// Dedup by source_session (idempotent — skip if already ingested)
|
|
701
707
|
if (source_session) {
|
|
702
708
|
const existing = db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE source_session = ?").get(source_session);
|
|
@@ -714,7 +720,7 @@ async function startServer(options = {}) {
|
|
|
714
720
|
sourceDomain: typeof source_domain === "string" ? source_domain : null,
|
|
715
721
|
sourceSession: source_session ?? undefined,
|
|
716
722
|
project: project ?? undefined,
|
|
717
|
-
memoryType:
|
|
723
|
+
memoryType: normalizedType ?? "experience",
|
|
718
724
|
// 0.16.x: privacy defaults to null (vestigial column). A legacy client
|
|
719
725
|
// that sends an explicit value is honored; absent → null.
|
|
720
726
|
privacy: typeof privacy === "string" ? privacy : null,
|
|
@@ -1061,9 +1067,9 @@ async function startServer(options = {}) {
|
|
|
1061
1067
|
sourceSession: sourcePrefix ? `${sourcePrefix}#${i}` : undefined,
|
|
1062
1068
|
project: project ?? undefined,
|
|
1063
1069
|
// #216: the distiller now classifies each entry as
|
|
1064
|
-
//
|
|
1070
|
+
// experience/knowledge/decisions via the [E]/[K]/[D] tag parsed in
|
|
1065
1071
|
// distiller.ts. Pre-#216 distiller output (no tag) defaults to
|
|
1066
|
-
//
|
|
1072
|
+
// experience in the parser, so this is backward compatible.
|
|
1067
1073
|
memoryType,
|
|
1068
1074
|
// 0.16.x: privacy defaults to null (vestigial column). A legacy
|
|
1069
1075
|
// client that sends an explicit value is honored; absent → null.
|
|
@@ -1111,19 +1117,24 @@ async function startServer(options = {}) {
|
|
|
1111
1117
|
fields.content = content;
|
|
1112
1118
|
if (project !== undefined)
|
|
1113
1119
|
fields.project = project;
|
|
1114
|
-
if (memory_type !== undefined)
|
|
1115
|
-
fields.memory_type = memory_type;
|
|
1116
1120
|
if (privacy !== undefined)
|
|
1117
1121
|
fields.privacy = privacy;
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
}
|
|
1122
|
-
const validTypes = ["episode", "lesson", "fact", "decision"];
|
|
1122
|
+
// Validate + normalize memory_type BEFORE adding to `fields` so the
|
|
1123
|
+
// empty-fields check below correctly counts a memory_type-only update.
|
|
1124
|
+
const validTypes = type_labels_js_1.ACCEPTED_MEMORY_TYPES;
|
|
1123
1125
|
if (memory_type !== undefined && !validTypes.includes(memory_type)) {
|
|
1124
1126
|
res.status(400).json({ error: `Invalid memory_type: ${memory_type}` });
|
|
1125
1127
|
return;
|
|
1126
1128
|
}
|
|
1129
|
+
// Normalize legacy raw enum (fact/episode/decision/lesson) to the
|
|
1130
|
+
// canonical term the DB stores (knowledge/experience/decisions/learnings).
|
|
1131
|
+
// Canonical values pass through unchanged.
|
|
1132
|
+
if (memory_type !== undefined)
|
|
1133
|
+
fields.memory_type = (0, type_labels_js_1.normalizeMemoryType)(memory_type);
|
|
1134
|
+
if (Object.keys(fields).length === 0) {
|
|
1135
|
+
res.status(400).json({ error: "No fields to update" });
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1127
1138
|
try {
|
|
1128
1139
|
storage.updateMemory(db, fullId, fields);
|
|
1129
1140
|
// Re-embed when content changes
|
package/dist/prompts.js
CHANGED
|
@@ -48,7 +48,7 @@ function reflection(memoriesBlock, recentLessons) {
|
|
|
48
48
|
Like human learning: we grow fastest when we reinforce what works AND correct what doesn't. A system that only learns from mistakes becomes overly cautious. A system that only learns from successes never improves. The combination multiplies.
|
|
49
49
|
|
|
50
50
|
GENERALITY BAR (read carefully — the most important rule):
|
|
51
|
-
Every lesson MUST be a generalizable operating principle that transfers across contexts, agents, and projects. It is NOT: an incident report, a changelog entry, a one-event fact, a tool-specific recipe, or a note about a named entity. If a memory is only interesting as "what happened today", it is an
|
|
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
|