@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 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)
@@ -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; }
@@ -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
- /** Default max classify-tier LLM calls (pairs evaluated) spent per nightly run. */
200
- export declare const DEFAULT_SUPERSESSION_MAX_CALLS = 30;
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;
@@ -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
- /** Default max classify-tier LLM calls (pairs evaluated) spent per nightly run. */
945
- exports.DEFAULT_SUPERSESSION_MAX_CALLS = 30;
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);
@@ -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): "episode" | "fact" | "decision" | null;
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<"episode" | "fact" | "decision" | null>;
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
@@ -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 one word: episode, fact, or decision. No prose, no punctuation.`);
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(/[*_`"'.\s]+$/, "")
103
- .trim()
104
- .toLowerCase();
105
- if (VALID_TYPES.has(cleaned)) {
106
- return cleaned;
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
- // ~8 tokens covers a single word + a little headroom.
121
- const r = await llm.completeClassify(prompt, 8);
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 type = await classifyMemoryType(row.content, llm);
214
- if (type === null) {
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
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.4",
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": {