@gamaze/hicortex 0.17.4 → 0.17.5
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 +8 -2
- package/dist/type-classify.js +33 -15
- 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
|
@@ -81,13 +81,19 @@ export declare function buildTypeClassifyPrompt(content: string): string;
|
|
|
81
81
|
* Returns null on anything unparseable or out-of-vocabulary so the caller can
|
|
82
82
|
* retry once (matching classify-domains' two-attempt discipline).
|
|
83
83
|
*/
|
|
84
|
-
export declare function parseTypeReply(reply: string):
|
|
84
|
+
export declare function parseTypeReply(reply: string): {
|
|
85
|
+
type: "episode" | "fact" | "decision";
|
|
86
|
+
score: number;
|
|
87
|
+
} | null;
|
|
85
88
|
/**
|
|
86
89
|
* Classify one memory's type. Two attempts (one call, one retry on a throw OR
|
|
87
90
|
* an unparseable reply). Returns the validated type, or null on infra error
|
|
88
91
|
* (caller leaves the memory untouched and retries via the cursor next run).
|
|
89
92
|
*/
|
|
90
|
-
export declare function classifyMemoryType(content: string, llm: LlmClient): Promise<
|
|
93
|
+
export declare function classifyMemoryType(content: string, llm: LlmClient): Promise<{
|
|
94
|
+
type: "episode" | "fact" | "decision";
|
|
95
|
+
score: number;
|
|
96
|
+
} | null>;
|
|
91
97
|
/**
|
|
92
98
|
* Run the classify-types pass. Returns a structured report.
|
|
93
99
|
* Throws on unrecoverable setup errors (client mode, no LLM) — the cursor
|
package/dist/type-classify.js
CHANGED
|
@@ -64,7 +64,7 @@ function buildTypeClassifyPrompt(content) {
|
|
|
64
64
|
const truncated = content.length > CLASSIFY_CONTENT_MAX_CHARS
|
|
65
65
|
? content.slice(0, CLASSIFY_CONTENT_MAX_CHARS) + "…"
|
|
66
66
|
: content;
|
|
67
|
-
return (`You are classifying a single memory by its TYPE.\n\n` +
|
|
67
|
+
return (`You are classifying a single memory by its TYPE and IMPORTANCE.\n\n` +
|
|
68
68
|
`TYPES:\n` +
|
|
69
69
|
`- episode: a specific event, interaction, or narrative — a one-time ` +
|
|
70
70
|
`occurrence ("tried X, failed because Y", a correction, a debugging session).\n` +
|
|
@@ -73,8 +73,14 @@ function buildTypeClassifyPrompt(content) {
|
|
|
73
73
|
`- decision: a choice made that future work builds on and a later decision ` +
|
|
74
74
|
`can supersede ("switched from gemma4 to qwen3.5", "adopted the graded-schema ` +
|
|
75
75
|
`tag model"). Not a fact (it can change) and not an episode (it persists).\n\n` +
|
|
76
|
+
`IMPORTANCE (0.0–1.0):\n` +
|
|
77
|
+
`- 0.8–1.0: load-bearing — a core fact or decision the agent must know.\n` +
|
|
78
|
+
`- 0.5–0.8: useful context — relevant to current and future work.\n` +
|
|
79
|
+
`- 0.2–0.5: marginal — situational, likely to fade.\n` +
|
|
80
|
+
`- 0.0–0.2: noise — low value, safe to forget.\n` +
|
|
81
|
+
`Facts and decisions tend to score higher than episodes (they persist).\n\n` +
|
|
76
82
|
`MEMORY:\n${truncated}\n\n` +
|
|
77
|
-
`Reply with ONLY
|
|
83
|
+
`Reply with ONLY: type importance (e.g. "fact 0.8"). No prose, no explanation.`);
|
|
78
84
|
}
|
|
79
85
|
/**
|
|
80
86
|
* Parse the model's reply into a validated type. Accepts the bare word
|
|
@@ -99,11 +105,23 @@ function parseTypeReply(reply) {
|
|
|
99
105
|
// Strip markdown emphasis, surrounding quotes/backticks, trailing punctuation.
|
|
100
106
|
cleaned = cleaned
|
|
101
107
|
.replace(/^[*_`"'\s]+/, "")
|
|
102
|
-
.replace(/[*_`"'
|
|
103
|
-
.trim()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
.replace(/[*_`"']+$/, "")
|
|
109
|
+
.trim();
|
|
110
|
+
// Expected format: "type score" (e.g. "fact 0.8"). Parse both.
|
|
111
|
+
const match = cleaned.toLowerCase().match(/^(episode|fact|decision)\s+([0-9]*\.?[0-9]+)/);
|
|
112
|
+
if (match) {
|
|
113
|
+
const type = match[1];
|
|
114
|
+
let score = parseFloat(match[2]);
|
|
115
|
+
if (isNaN(score) || score < 0)
|
|
116
|
+
score = 0.5;
|
|
117
|
+
if (score > 1)
|
|
118
|
+
score = 1;
|
|
119
|
+
return { type, score };
|
|
120
|
+
}
|
|
121
|
+
// Backward compat: bare type word with no score (old prompt output).
|
|
122
|
+
const bare = cleaned.toLowerCase().replace(/[.\s]+$/, "");
|
|
123
|
+
if (VALID_TYPES.has(bare)) {
|
|
124
|
+
return { type: bare, score: 0.5 };
|
|
107
125
|
}
|
|
108
126
|
return null;
|
|
109
127
|
}
|
|
@@ -117,8 +135,8 @@ async function classifyMemoryType(content, llm) {
|
|
|
117
135
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
118
136
|
let raw;
|
|
119
137
|
try {
|
|
120
|
-
// ~
|
|
121
|
-
const r = await llm.completeClassify(prompt,
|
|
138
|
+
// ~20 tokens covers "type score" + headroom for models that add labels.
|
|
139
|
+
const r = await llm.completeClassify(prompt, 20);
|
|
122
140
|
raw = r.text;
|
|
123
141
|
}
|
|
124
142
|
catch (err) {
|
|
@@ -210,8 +228,8 @@ async function runClassifyTypes(options = {}) {
|
|
|
210
228
|
// Classify (network) OUTSIDE the write transaction; collect results.
|
|
211
229
|
const writes = [];
|
|
212
230
|
for (const row of rows) {
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
231
|
+
const result = await classifyMemoryType(row.content, llm);
|
|
232
|
+
if (result === null) {
|
|
215
233
|
// Infra error OR two unparseable replies — stop scanning; leave this
|
|
216
234
|
// row untouched for retry. (Two unparseable replies is rare; treating
|
|
217
235
|
// it as an abort rather than a skip means the cursor does not advance
|
|
@@ -222,21 +240,21 @@ async function runClassifyTypes(options = {}) {
|
|
|
222
240
|
break;
|
|
223
241
|
}
|
|
224
242
|
scannedInBatch++;
|
|
225
|
-
if (type === row.memory_type) {
|
|
243
|
+
if (result.type === row.memory_type) {
|
|
226
244
|
batchUnchanged++;
|
|
227
245
|
}
|
|
228
246
|
else {
|
|
229
247
|
batchReclassified++;
|
|
230
248
|
}
|
|
231
|
-
writes.push({ id: row.id, type });
|
|
249
|
+
writes.push({ id: row.id, type: result.type, score: result.score });
|
|
232
250
|
committedRowid = row.__rowid;
|
|
233
251
|
}
|
|
234
252
|
// Commit the resolved writes, then persist the cursor at the last fully
|
|
235
253
|
// resolved rowid (crash-safe + infra-abort-safe: a re-run resumes there).
|
|
236
|
-
const updateStmt = db.prepare("UPDATE memories SET memory_type = ? WHERE id = ?");
|
|
254
|
+
const updateStmt = db.prepare("UPDATE memories SET memory_type = ?, base_strength = ? WHERE id = ?");
|
|
237
255
|
const tx = db.transaction(() => {
|
|
238
256
|
for (const w of writes)
|
|
239
|
-
updateStmt.run(w.type, w.id);
|
|
257
|
+
updateStmt.run(w.type, w.score, w.id);
|
|
240
258
|
});
|
|
241
259
|
tx();
|
|
242
260
|
(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.5",
|
|
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": {
|