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