@gamaze/hicortex 0.17.4 → 0.17.6
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 +9 -0
- package/assets/dashboard.html +1 -1
- package/dist/consolidate.d.ts +11 -2
- package/dist/consolidate.js +13 -4
- package/dist/type-classify.d.ts +21 -11
- package/dist/type-classify.js +55 -26
- package/hermes-plugin/hicortex/plugin.yaml +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @gamaze/hicortex — Self-Learning Memory for AI Agents
|
|
2
2
|
|
|
3
|
+
<img src="https://raw.githubusercontent.com/gamaze-labs/hicortex/main/docs/dashboard-composition.png" alt="Hicortex dashboard — live screenshot: memory-type composition (episodes / lessons / facts / decisions)" width="800">
|
|
4
|
+
|
|
3
5
|
Your agents learn from every session — successes and mistakes. Hicortex captures experiences, distills lessons, and applies them automatically. Connect multiple agents to shared memory and they improve together, overnight.
|
|
4
6
|
|
|
5
7
|
Works with **Hermes**, **OpenClaw**, **Claude Code**, **Pi**, and any MCP-compatible agent.
|
|
@@ -104,6 +106,12 @@ npx @gamaze/hicortex classify-domains # classify unfiled/stale memo
|
|
|
104
106
|
npx @gamaze/hicortex classify-domains --all # reclassify every memory
|
|
105
107
|
npx @gamaze/hicortex classify-domains --batch 100 # memories per batch (default: 200)
|
|
106
108
|
npx @gamaze/hicortex classify-domains --reset # restart from the beginning (ignore saved cursor)
|
|
109
|
+
|
|
110
|
+
# Reclassify memory types (episode/fact/decision) — fixes the 98%-episode default
|
|
111
|
+
npx @gamaze/hicortex classify-types # reclassify all episodes (default scope)
|
|
112
|
+
npx @gamaze/hicortex classify-types --all # reclassify every memory
|
|
113
|
+
npx @gamaze/hicortex classify-types --batch 100 # memories per batch (default: 200)
|
|
114
|
+
npx @gamaze/hicortex classify-types --reset # restart from the beginning
|
|
107
115
|
```
|
|
108
116
|
|
|
109
117
|
The run is resumable — interrupt it any time and it continues where it stopped. New memories are classified automatically by the nightly; the backfill is only needed once for a pre-existing corpus or after you reshape your domain list.
|
|
@@ -157,6 +165,7 @@ npx @gamaze/hicortex nightly # Run distill + consolidate (full
|
|
|
157
165
|
npx @gamaze/hicortex nightly --capture-only # Capture only, skip consolidation (safe for sub-daily runs)
|
|
158
166
|
npx @gamaze/hicortex nightly --dry-run # Preview without changes
|
|
159
167
|
npx @gamaze/hicortex classify-domains # Backfill domain tags over the corpus (see Memory Domains & Tags)
|
|
168
|
+
npx @gamaze/hicortex classify-types # Reclassify memory types (episode/fact/decision)
|
|
160
169
|
npx @gamaze/hicortex dedup # Preview near-duplicate memory clusters (dry run, no changes)
|
|
161
170
|
npx @gamaze/hicortex dedup --apply # Merge near-duplicate clusters (backs up the DB first)
|
|
162
171
|
npx @gamaze/hicortex context show [name] # Print the standing context layer (see Context Layer)
|
package/assets/dashboard.html
CHANGED
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
.bar-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; font-size: 13px; }
|
|
78
78
|
.bar-row .k { width: 140px; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
79
79
|
.bar-row .track { flex: 1; height: 14px; background: rgba(128,128,128,0.12); border-radius: 3px; overflow: hidden; }
|
|
80
|
-
.bar-row .fill { height: 100%; background: var(--accent); }
|
|
80
|
+
.bar-row .fill { display: block; height: 100%; background: var(--accent); }
|
|
81
81
|
.bar-row .n { width: 50px; text-align: right; color: var(--text-dim); font-variant-numeric: tabular-nums; }
|
|
82
82
|
.line { font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--text); white-space: pre-wrap; word-break: break-word; }
|
|
83
83
|
.line a { color: var(--accent); text-decoration: none; }
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -196,8 +196,17 @@ export declare function classifyLinkCandidates(candidates: LinkCandidate[], _llm
|
|
|
196
196
|
export declare function classifyRelationship(source: Memory, target: Memory, similarity: number): string;
|
|
197
197
|
/** Default minimum COSINE similarity for a supersession candidate pair. */
|
|
198
198
|
export declare const DEFAULT_SUPERSESSION_MIN_SIMILARITY = 0.8;
|
|
199
|
-
/**
|
|
200
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Default max classify-tier LLM calls (pairs evaluated) spent per nightly run.
|
|
201
|
+
* 0 = no separate cap — supersession shares the consolidation budget
|
|
202
|
+
* (CONSOLIDATE_MAX_LLM_CALLS, default 5000) like every other stage. The old
|
|
203
|
+
* default of 30 was set when the corpus had 14 decisions; with the distiller
|
|
204
|
+
* now classifying types correctly (#216), decisions are common and the cap
|
|
205
|
+
* was throttling supersession to a crawl. On a local free model there is no
|
|
206
|
+
* per-call cost to defend against — the binding constraint is the wall-clock
|
|
207
|
+
* timeout (TimeoutStartSec), not call count.
|
|
208
|
+
*/
|
|
209
|
+
export declare const DEFAULT_SUPERSESSION_MAX_CALLS = 0;
|
|
201
210
|
export interface SupersessionOptions {
|
|
202
211
|
minSimilarity?: number;
|
|
203
212
|
maxCalls?: number;
|
package/dist/consolidate.js
CHANGED
|
@@ -941,8 +941,17 @@ function stageHubBoost(db, dryRun) {
|
|
|
941
941
|
// note below); it never mis-links.
|
|
942
942
|
/** Default minimum COSINE similarity for a supersession candidate pair. */
|
|
943
943
|
exports.DEFAULT_SUPERSESSION_MIN_SIMILARITY = 0.8;
|
|
944
|
-
/**
|
|
945
|
-
|
|
944
|
+
/**
|
|
945
|
+
* Default max classify-tier LLM calls (pairs evaluated) spent per nightly run.
|
|
946
|
+
* 0 = no separate cap — supersession shares the consolidation budget
|
|
947
|
+
* (CONSOLIDATE_MAX_LLM_CALLS, default 5000) like every other stage. The old
|
|
948
|
+
* default of 30 was set when the corpus had 14 decisions; with the distiller
|
|
949
|
+
* now classifying types correctly (#216), decisions are common and the cap
|
|
950
|
+
* was throttling supersession to a crawl. On a local free model there is no
|
|
951
|
+
* per-call cost to defend against — the binding constraint is the wall-clock
|
|
952
|
+
* timeout (TimeoutStartSec), not call count.
|
|
953
|
+
*/
|
|
954
|
+
exports.DEFAULT_SUPERSESSION_MAX_CALLS = 0;
|
|
946
955
|
/** Default multiplier applied to a superseded memory's base_strength. */
|
|
947
956
|
/** Floor under which a superseded memory's base_strength never drops. */
|
|
948
957
|
/** Neighbor pool size before shape/older/similarity filtering narrows to top 5. */
|
|
@@ -1096,7 +1105,7 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1096
1105
|
let callsUsed = 0;
|
|
1097
1106
|
let cursor = startCursor;
|
|
1098
1107
|
for (const candidate of rows) {
|
|
1099
|
-
if (!dryRun && (callsUsed >= maxCalls || budget.exhausted))
|
|
1108
|
+
if (!dryRun && ((maxCalls > 0 && callsUsed >= maxCalls) || budget.exhausted))
|
|
1100
1109
|
break;
|
|
1101
1110
|
scanned++;
|
|
1102
1111
|
let neighbors;
|
|
@@ -1115,7 +1124,7 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1115
1124
|
}
|
|
1116
1125
|
if (dryRun)
|
|
1117
1126
|
continue; // preview only — no LLM call, no write
|
|
1118
|
-
if (callsUsed >= maxCalls || !budget.use("supersession"))
|
|
1127
|
+
if ((maxCalls > 0 && callsUsed >= maxCalls) || !budget.use("supersession"))
|
|
1119
1128
|
break;
|
|
1120
1129
|
callsUsed++;
|
|
1121
1130
|
const { verdict, usage } = await classifySupersession(llm, neighbor.content, candidate.content);
|
package/dist/type-classify.d.ts
CHANGED
|
@@ -15,16 +15,20 @@
|
|
|
15
15
|
* ------------
|
|
16
16
|
* Walks memories ordered by rowid in batches (default 200). Default scope =
|
|
17
17
|
* episodes only (`memory_type = 'episode'`); `--all` reclassifies every memory
|
|
18
|
-
* regardless of current type. For each memory,
|
|
19
|
-
* the model to classify the content as
|
|
20
|
-
*
|
|
21
|
-
* runs inside a per-batch
|
|
22
|
-
*
|
|
23
|
-
* infra-abort-safe (same
|
|
18
|
+
* regardless of current type **except lessons** (see below). For each memory,
|
|
19
|
+
* ONE constrained LLM call asks the model to classify the content as
|
|
20
|
+
* episode / fact / decision. The reply is parsed + validated, and
|
|
21
|
+
* `UPDATE memories SET memory_type = ? WHERE id = ?` runs inside a per-batch
|
|
22
|
+
* transaction. The cursor (`typeCursor` in state.json) advances to the last
|
|
23
|
+
* committed rowid after each batch — crash-safe and infra-abort-safe (same
|
|
24
|
+
* discipline as classify-domains).
|
|
24
25
|
*
|
|
25
|
-
* Lessons are NEVER
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Lessons are NEVER touched here: the reflection stage owns them. The `--all`
|
|
27
|
+
* scope explicitly excludes `memory_type = 'lesson'` — this is the primary
|
|
28
|
+
* defence. (The prompt asks only for episode/fact/decision, so a lesson that
|
|
29
|
+
* DID enter scope would be overwritten to E/F/D — the model never replies
|
|
30
|
+
* "lesson". `parseTypeReply`'s rejection of a "lesson" reply is a backstop, not
|
|
31
|
+
* the main guard.)
|
|
28
32
|
*
|
|
29
33
|
* This command does NOT use the consolidation budget — it is a standalone CLI,
|
|
30
34
|
* not a nightly stage.
|
|
@@ -81,13 +85,19 @@ export declare function buildTypeClassifyPrompt(content: string): string;
|
|
|
81
85
|
* Returns null on anything unparseable or out-of-vocabulary so the caller can
|
|
82
86
|
* retry once (matching classify-domains' two-attempt discipline).
|
|
83
87
|
*/
|
|
84
|
-
export declare function parseTypeReply(reply: string):
|
|
88
|
+
export declare function parseTypeReply(reply: string): {
|
|
89
|
+
type: "episode" | "fact" | "decision";
|
|
90
|
+
score: number;
|
|
91
|
+
} | null;
|
|
85
92
|
/**
|
|
86
93
|
* Classify one memory's type. Two attempts (one call, one retry on a throw OR
|
|
87
94
|
* an unparseable reply). Returns the validated type, or null on infra error
|
|
88
95
|
* (caller leaves the memory untouched and retries via the cursor next run).
|
|
89
96
|
*/
|
|
90
|
-
export declare function classifyMemoryType(content: string, llm: LlmClient): Promise<
|
|
97
|
+
export declare function classifyMemoryType(content: string, llm: LlmClient): Promise<{
|
|
98
|
+
type: "episode" | "fact" | "decision";
|
|
99
|
+
score: number;
|
|
100
|
+
} | null>;
|
|
91
101
|
/**
|
|
92
102
|
* Run the classify-types pass. Returns a structured report.
|
|
93
103
|
* Throws on unrecoverable setup errors (client mode, no LLM) — the cursor
|
package/dist/type-classify.js
CHANGED
|
@@ -16,16 +16,20 @@
|
|
|
16
16
|
* ------------
|
|
17
17
|
* Walks memories ordered by rowid in batches (default 200). Default scope =
|
|
18
18
|
* episodes only (`memory_type = 'episode'`); `--all` reclassifies every memory
|
|
19
|
-
* regardless of current type. For each memory,
|
|
20
|
-
* the model to classify the content as
|
|
21
|
-
*
|
|
22
|
-
* runs inside a per-batch
|
|
23
|
-
*
|
|
24
|
-
* infra-abort-safe (same
|
|
19
|
+
* regardless of current type **except lessons** (see below). For each memory,
|
|
20
|
+
* ONE constrained LLM call asks the model to classify the content as
|
|
21
|
+
* episode / fact / decision. The reply is parsed + validated, and
|
|
22
|
+
* `UPDATE memories SET memory_type = ? WHERE id = ?` runs inside a per-batch
|
|
23
|
+
* transaction. The cursor (`typeCursor` in state.json) advances to the last
|
|
24
|
+
* committed rowid after each batch — crash-safe and infra-abort-safe (same
|
|
25
|
+
* discipline as classify-domains).
|
|
25
26
|
*
|
|
26
|
-
* Lessons are NEVER
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* Lessons are NEVER touched here: the reflection stage owns them. The `--all`
|
|
28
|
+
* scope explicitly excludes `memory_type = 'lesson'` — this is the primary
|
|
29
|
+
* defence. (The prompt asks only for episode/fact/decision, so a lesson that
|
|
30
|
+
* DID enter scope would be overwritten to E/F/D — the model never replies
|
|
31
|
+
* "lesson". `parseTypeReply`'s rejection of a "lesson" reply is a backstop, not
|
|
32
|
+
* the main guard.)
|
|
29
33
|
*
|
|
30
34
|
* This command does NOT use the consolidation budget — it is a standalone CLI,
|
|
31
35
|
* not a nightly stage.
|
|
@@ -64,7 +68,7 @@ function buildTypeClassifyPrompt(content) {
|
|
|
64
68
|
const truncated = content.length > CLASSIFY_CONTENT_MAX_CHARS
|
|
65
69
|
? content.slice(0, CLASSIFY_CONTENT_MAX_CHARS) + "…"
|
|
66
70
|
: content;
|
|
67
|
-
return (`You are classifying a single memory by its TYPE.\n\n` +
|
|
71
|
+
return (`You are classifying a single memory by its TYPE and IMPORTANCE.\n\n` +
|
|
68
72
|
`TYPES:\n` +
|
|
69
73
|
`- episode: a specific event, interaction, or narrative — a one-time ` +
|
|
70
74
|
`occurrence ("tried X, failed because Y", a correction, a debugging session).\n` +
|
|
@@ -73,8 +77,14 @@ function buildTypeClassifyPrompt(content) {
|
|
|
73
77
|
`- decision: a choice made that future work builds on and a later decision ` +
|
|
74
78
|
`can supersede ("switched from gemma4 to qwen3.5", "adopted the graded-schema ` +
|
|
75
79
|
`tag model"). Not a fact (it can change) and not an episode (it persists).\n\n` +
|
|
80
|
+
`IMPORTANCE (0.0–1.0):\n` +
|
|
81
|
+
`- 0.8–1.0: load-bearing — a core fact or decision the agent must know.\n` +
|
|
82
|
+
`- 0.5–0.8: useful context — relevant to current and future work.\n` +
|
|
83
|
+
`- 0.2–0.5: marginal — situational, likely to fade.\n` +
|
|
84
|
+
`- 0.0–0.2: noise — low value, safe to forget.\n` +
|
|
85
|
+
`Facts and decisions tend to score higher than episodes (they persist).\n\n` +
|
|
76
86
|
`MEMORY:\n${truncated}\n\n` +
|
|
77
|
-
`Reply with ONLY
|
|
87
|
+
`Reply with ONLY: type importance (e.g. "fact 0.8"). No prose, no explanation.`);
|
|
78
88
|
}
|
|
79
89
|
/**
|
|
80
90
|
* Parse the model's reply into a validated type. Accepts the bare word
|
|
@@ -99,11 +109,23 @@ function parseTypeReply(reply) {
|
|
|
99
109
|
// Strip markdown emphasis, surrounding quotes/backticks, trailing punctuation.
|
|
100
110
|
cleaned = cleaned
|
|
101
111
|
.replace(/^[*_`"'\s]+/, "")
|
|
102
|
-
.replace(/[*_`"'
|
|
103
|
-
.trim()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
.replace(/[*_`"']+$/, "")
|
|
113
|
+
.trim();
|
|
114
|
+
// Expected format: "type score" (e.g. "fact 0.8"). Parse both.
|
|
115
|
+
const match = cleaned.toLowerCase().match(/^(episode|fact|decision)\s+([0-9]*\.?[0-9]+)/);
|
|
116
|
+
if (match) {
|
|
117
|
+
const type = match[1];
|
|
118
|
+
let score = parseFloat(match[2]);
|
|
119
|
+
if (isNaN(score) || score < 0)
|
|
120
|
+
score = 0.5;
|
|
121
|
+
if (score > 1)
|
|
122
|
+
score = 1;
|
|
123
|
+
return { type, score };
|
|
124
|
+
}
|
|
125
|
+
// Backward compat: bare type word with no score (old prompt output).
|
|
126
|
+
const bare = cleaned.toLowerCase().replace(/[.\s]+$/, "");
|
|
127
|
+
if (VALID_TYPES.has(bare)) {
|
|
128
|
+
return { type: bare, score: 0.5 };
|
|
107
129
|
}
|
|
108
130
|
return null;
|
|
109
131
|
}
|
|
@@ -117,8 +139,8 @@ async function classifyMemoryType(content, llm) {
|
|
|
117
139
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
118
140
|
let raw;
|
|
119
141
|
try {
|
|
120
|
-
// ~
|
|
121
|
-
const r = await llm.completeClassify(prompt,
|
|
142
|
+
// ~20 tokens covers "type score" + headroom for models that add labels.
|
|
143
|
+
const r = await llm.completeClassify(prompt, 20);
|
|
122
144
|
raw = r.text;
|
|
123
145
|
}
|
|
124
146
|
catch (err) {
|
|
@@ -187,8 +209,15 @@ async function runClassifyTypes(options = {}) {
|
|
|
187
209
|
report.cursor = cursor;
|
|
188
210
|
console.log(`[hicortex] classify-types starting: scope ${all ? "ALL" : "episodes only"}, ` +
|
|
189
211
|
`batch ${batchSize}, cursor ${cursor}${options.reset ? " (reset)" : ""}`);
|
|
190
|
-
// Scope filter: default = episodes only; --all = everything.
|
|
191
|
-
|
|
212
|
+
// Scope filter: default = episodes only; --all = everything EXCEPT lessons.
|
|
213
|
+
// Lessons are owned by the reflection stage — they must never be
|
|
214
|
+
// reclassified here. (The prompt asks only for episode/fact/decision, so a
|
|
215
|
+
// lesson row in scope gets overwritten: the model never replies "lesson".
|
|
216
|
+
// The scope exclusion is therefore the real guard; parseTypeReply's
|
|
217
|
+
// "lesson" rejection is a backstop, not the primary defence.)
|
|
218
|
+
const scopeSql = all
|
|
219
|
+
? "rowid > ? AND (memory_type IS NULL OR memory_type != 'lesson')"
|
|
220
|
+
: "rowid > ? AND memory_type = 'episode'";
|
|
192
221
|
const batchStmt = db.prepare(`SELECT rowid AS __rowid, id, content, memory_type FROM memories
|
|
193
222
|
WHERE ${scopeSql} ORDER BY rowid ASC LIMIT ?`);
|
|
194
223
|
// Set true when the classifier returns null (infra error): finish the
|
|
@@ -210,8 +239,8 @@ async function runClassifyTypes(options = {}) {
|
|
|
210
239
|
// Classify (network) OUTSIDE the write transaction; collect results.
|
|
211
240
|
const writes = [];
|
|
212
241
|
for (const row of rows) {
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
242
|
+
const result = await classifyMemoryType(row.content, llm);
|
|
243
|
+
if (result === null) {
|
|
215
244
|
// Infra error OR two unparseable replies — stop scanning; leave this
|
|
216
245
|
// row untouched for retry. (Two unparseable replies is rare; treating
|
|
217
246
|
// it as an abort rather than a skip means the cursor does not advance
|
|
@@ -222,21 +251,21 @@ async function runClassifyTypes(options = {}) {
|
|
|
222
251
|
break;
|
|
223
252
|
}
|
|
224
253
|
scannedInBatch++;
|
|
225
|
-
if (type === row.memory_type) {
|
|
254
|
+
if (result.type === row.memory_type) {
|
|
226
255
|
batchUnchanged++;
|
|
227
256
|
}
|
|
228
257
|
else {
|
|
229
258
|
batchReclassified++;
|
|
230
259
|
}
|
|
231
|
-
writes.push({ id: row.id, type });
|
|
260
|
+
writes.push({ id: row.id, type: result.type, score: result.score });
|
|
232
261
|
committedRowid = row.__rowid;
|
|
233
262
|
}
|
|
234
263
|
// Commit the resolved writes, then persist the cursor at the last fully
|
|
235
264
|
// resolved rowid (crash-safe + infra-abort-safe: a re-run resumes there).
|
|
236
|
-
const updateStmt = db.prepare("UPDATE memories SET memory_type = ? WHERE id = ?");
|
|
265
|
+
const updateStmt = db.prepare("UPDATE memories SET memory_type = ?, base_strength = ? WHERE id = ?");
|
|
237
266
|
const tx = db.transaction(() => {
|
|
238
267
|
for (const w of writes)
|
|
239
|
-
updateStmt.run(w.type, w.id);
|
|
268
|
+
updateStmt.run(w.type, w.score, w.id);
|
|
240
269
|
});
|
|
241
270
|
tx();
|
|
242
271
|
(0, state_js_1.updateState)((s) => { s.typeCursor = committedRowid; }, stateDir);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: hicortex
|
|
2
|
-
version: 0.7.
|
|
2
|
+
version: 0.7.3
|
|
3
3
|
description: "Self-learning memory for Hermes agents — every session is distilled into lessons overnight, and your agent wakes up wiser. Pushes a compact per-turn recall index (lazy-loaded with hicortex_get), injects fresh lessons plus a per-agent standing context block, and exposes the full 9-tool memory surface (search, get, recent, ingest, lessons, index, graph, update, delete) via a shared Hicortex server. Stdlib-only."
|
|
4
4
|
pip_dependencies: []
|
|
5
5
|
hooks: []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.6",
|
|
4
4
|
"description": "Self-learning memory for AI agents — experience captured automatically, distilled into lessons overnight, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|