@gamaze/hicortex 0.16.0 → 0.16.1

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/dist/index.js CHANGED
@@ -163,10 +163,11 @@ async function buildLessonsBlock(project) {
163
163
  if (selected.length === 0)
164
164
  return null;
165
165
  const formatted = selected.map((l) => {
166
- const titleMatch = l.content.match(/## Lesson: (.+)/);
167
166
  const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
168
167
  const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
169
- const title = titleMatch ? titleMatch[1] : l.content.slice(0, 150);
168
+ // First line, with any legacy `## Lesson:` prefix stripped — new lessons
169
+ // are stored topic-first without the prefix (memory_type carries the type).
170
+ const title = l.content.replace(/^##\s*Lesson:\s*/i, "").split("\n")[0].slice(0, 150);
170
171
  const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
171
172
  return `- ${title}${meta ? ` (${meta})` : ""}`;
172
173
  });
@@ -90,10 +90,11 @@ async function fetchLessonsBlock(cfg) {
90
90
  const project = (0, node_path_1.basename)(process.cwd()) || null;
91
91
  const selected = await (0, extensions_js_1.getLessonSelector)().select(data.lessons, { maxLessons, moduleIndex, project });
92
92
  const lessonLines = selected.map((l) => {
93
- const titleMatch = l.content.match(/## Lesson: (.+)/);
94
93
  const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
95
94
  const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
96
- const title = titleMatch ? titleMatch[1] : l.content.slice(0, 150);
95
+ // First line, with any legacy `## Lesson:` prefix stripped — new lessons
96
+ // are stored topic-first without the prefix (memory_type carries the type).
97
+ const title = l.content.replace(/^##\s*Lesson:\s*/i, "").split("\n")[0].slice(0, 150);
97
98
  const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
98
99
  return `- ${title}${meta ? ` (${meta})` : ""}`;
99
100
  });
@@ -494,6 +494,7 @@ async function startServer(options = {}) {
494
494
  minSimilarity: savedConfig?.recallMinSimilarity,
495
495
  maxItems: savedConfig?.recallMaxItems,
496
496
  minPromptLength: savedConfig?.recallMinPromptChars,
497
+ titleChars: savedConfig?.recallTitleChars,
497
498
  };
498
499
  memoryInstructionsEnabled = savedConfig?.memoryInstructions !== false;
499
500
  if (resolvedAgents.dropped.length > 0) {
package/dist/prompts.js CHANGED
@@ -108,25 +108,39 @@ EXTRACT into this markdown format:
108
108
  ## Classification: [pick one: PUBLIC / WORK / PERSONAL / SENSITIVE]
109
109
 
110
110
  ### Decisions Made
111
- - [decision]: [reasoning] (${date})
111
+ - [SUBJECT]: [decision] — [reasoning] (${date})
112
112
 
113
113
  ### Facts Learned
114
- - [fact]: [context/source] (${date})
114
+ - [SUBJECT]: [fact] — [context/source] (${date})
115
115
 
116
116
  ### Problems & Solutions
117
- - [problem] → [solution that worked] (${date})
117
+ - [SUBJECT]: [problem] → [solution that worked] (${date})
118
118
 
119
119
  ### Project State Changes
120
- - [what changed]: [from → to] (${date})
120
+ - [SUBJECT]: [what changed], [from → to] (${date})
121
121
 
122
122
  ### Key Entities & Relationships
123
123
  - [entity A] → [relationship] → [entity B] (${date})
124
124
 
125
125
  ### Corrections & Rejections
126
- - [what AI proposed] → [why rejected/corrected] → [what user wanted instead] (${date})
126
+ - [SUBJECT]: [what AI proposed] → [why rejected/corrected] → [what user wanted instead] (${date})
127
127
  (Include: tool use denials, "no/wrong/redo", style feedback, approach rejections,
128
128
  user corrections of AI assumptions, quality complaints like "too verbose")
129
129
 
130
+ TOPIC-FIRST RULE (critical — read carefully):
131
+ Every item MUST begin with its [SUBJECT]: the concrete thing it is about — the
132
+ system, file, component, decision area, or entity. The subject is what a future
133
+ reader would search for.
134
+ - Write: "Electrical load calculation: don't bundle unknown loads into one figure — user rejected the estimate"
135
+ - NOT: "User rejected AI's bundling of unknown loads"
136
+ - Write: "Nightly capture (Hermes): cron sessions are excluded — source='cron' is skipped before distillation"
137
+ - NOT: "Discovered that cron sessions are filtered out"
138
+ Reason: each item's first words become the memory's one-line index entry AND
139
+ dominate its search embedding. An item that opens with a category label, a
140
+ sentiment ("Strong Negative"), or "User rejected…" is unfindable — it matches
141
+ every emotionally-similar prompt and no topically-relevant one. Front-load the
142
+ subject; put reaction, intensity and reasoning AFTER it.
143
+
130
144
  RULES:
131
145
  - Extract MAX 20 items total (quality over quantity)
132
146
  - Each must be useful if recalled in a future session
@@ -136,7 +150,9 @@ RULES:
136
150
  - PRIORITIZE Corrections & Rejections — these are high-value signals for learning
137
151
  what the user does NOT want. Even a single "no" or style correction is worth extracting.
138
152
  - Strong language or profanity from the user is a high-intensity signal — it indicates
139
- the correction matters deeply. Note the intensity in the extraction.
153
+ the correction matters deeply. Note the intensity AFTER the subject, never before it
154
+ (e.g. "Pricing tiers: strongly rejected per-agent billing — …", not
155
+ "[Strong Negative] User rejected per-agent billing"). The subject always comes first.
140
156
  - PRIVACY CLASSIFICATION (one of):
141
157
  - PUBLIC: general tech knowledge, open-source patterns, publicly available info
142
158
  - WORK: project-specific decisions, architecture choices, client/business context
@@ -25,13 +25,31 @@ import { SessionRecallRegistry } from "./recall-registry.js";
25
25
  export interface RecallIndexOptions {
26
26
  /** Minimum measured cosine for vector-only candidates (config
27
27
  * `recallMinSimilarity`). FTS-matched candidates pass regardless — a BM25
28
- * text match is direct evidence of relevance. Default 0.55 (the neutral
29
- * placeholder similarity is 0.5; anything at/below that is noise). */
28
+ * text match is direct evidence of relevance. Default 0.62 (raised from 0.55
29
+ * on 2026-08-03 per a 0.01-step floor sweep on the rewritten corpus): steady
30
+ * ~3:1 noise:signal removal with no knee; 0.62 = +2.2pts precision, 10/98
31
+ * prompts silent, sits below the 0.63 local pessimum. The floor is a noise
32
+ * dial, NOT a silence mechanism — at 0.62 each correctly-silenced empty prompt
33
+ * comes with ~1.5 wrongly-silenced (real signal); a non-cosine gate is the
34
+ * real silence fix (eval #3 §4). */
30
35
  minSimilarity?: number;
31
- /** Max index lines per response (config `recallMaxItems`). Default 6. */
36
+ /** Max index lines per response (config `recallMaxItems`). Default 5
37
+ * (lowered from 6 on 2026-08-03). Per-slot decomposition at floor 0.62:
38
+ * slot 6 gives NO prompt its first relevant memory — "6 is wrong" is the
39
+ * robust, prompt-set-independent finding, and 5 captures it. The K-sweep
40
+ * is monotone (precision@4 33.7% > @6 30.6% > @8 28.3%), so 4 is
41
+ * lower-noise — but the 4-vs-5 distinction rests on 5 of 98 prompts and is
42
+ * overfitting-fragile (K and the floor were tuned on the same set); 5 hedges
43
+ * with coverage at modest cost. Lower to 4 if a fresh-prompt eval replicates. */
32
44
  maxItems?: number;
33
45
  /** Prompts shorter than this are skipped (continuations, "yes", "do it"). */
34
46
  minPromptLength?: number;
47
+ /** Max chars of the memory's first line shown in an index entry (config
48
+ * `recallTitleChars`). Default 100 (reverted from 150 on 2026-08-03): the
49
+ * full-corpus relevance eval (#3, §5) found 100 vs 150 statistically
50
+ * identical (0.6pts apart, N=40, full CI overlap); 100 saves ~13% tokens
51
+ * per block. */
52
+ titleChars?: number;
35
53
  }
36
54
  export interface RecallIndexResult {
37
55
  status: number;
@@ -39,7 +57,28 @@ export interface RecallIndexResult {
39
57
  }
40
58
  /** First content line, de-markdowned and truncated — the index line title. */
41
59
  export declare function memoryTitle(content: string, maxLen?: number): string;
42
- /** Relevance gate: real text match, or measured cosine above the floor. */
60
+ /**
61
+ * Render one production index line. Exported (2026-08-02, relevance eval #v2)
62
+ * so the eval can measure the REAL rendered surface instead of reimplementing
63
+ * it — `maxLen` threads through to `memoryTitle` unchanged (default
64
+ * DEFAULT_TITLE_CHARS = 100, config `recallTitleChars`) so the eval's snippet-length
65
+ * sweep (spec §4.2) can call this SAME function at 100/150/title1sent without
66
+ * duplicating the date/scope/agent/type meta-line logic.
67
+ */
68
+ export declare function formatIndexLine(r: MemorySearchResult & {
69
+ domain?: string | null;
70
+ }, maxLen?: number): string;
71
+ /**
72
+ * Relevance gate: a real BM25 text match (FTS) passes unconditionally; a
73
+ * vector-only candidate must clear `minSimilarity`.
74
+ *
75
+ * NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
76
+ * weight toward FTS-sourced entries. In practice FTS is currently inert on
77
+ * real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
78
+ * a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
79
+ * is safe as measured. But FTS quality is unmeasured; if FTS starts firing
80
+ * (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
81
+ */
43
82
  export declare function passesRelevanceGate(r: MemorySearchResult, minSimilarity: number): boolean;
44
83
  /** Recall filters a client may push per request (#193 review F1): a scoped
45
84
  * plugin (Hermes privacy_filter / default_project) must be able to narrow
@@ -55,6 +55,7 @@ var __importStar = (this && this.__importStar) || (function () {
55
55
  })();
56
56
  Object.defineProperty(exports, "__esModule", { value: true });
57
57
  exports.memoryTitle = memoryTitle;
58
+ exports.formatIndexLine = formatIndexLine;
58
59
  exports.passesRelevanceGate = passesRelevanceGate;
59
60
  exports.parseStringListParam = parseStringListParam;
60
61
  exports.parsePrivacyParam = parsePrivacyParam;
@@ -62,13 +63,25 @@ exports.handleRecallIndex = handleRecallIndex;
62
63
  exports.handleMemoryGet = handleMemoryGet;
63
64
  exports.formatMemoryGetText = formatMemoryGetText;
64
65
  const storage = __importStar(require("./storage.js"));
65
- const DEFAULT_MIN_SIMILARITY = 0.55;
66
- const DEFAULT_MAX_ITEMS = 6;
66
+ /** Relevance-gate floor for vector-only candidates (config `recallMinSimilarity`).
67
+ * 0.62 (was 0.55; raised 2026-08-03 on the fine-grain floor sweep — see the
68
+ * minSimilarity doc above). */
69
+ const DEFAULT_MIN_SIMILARITY = 0.62;
70
+ /** Max index lines per pushed recall block (config `recallMaxItems`).
71
+ * 5 (was 6; lowered 2026-08-03 — slot 6 is pure padding at floor 0.62). */
72
+ const DEFAULT_MAX_ITEMS = 5;
67
73
  const DEFAULT_MIN_PROMPT_LENGTH = 20;
68
- /** Retrieve more than maxItems so gating + dedup still leave a full menu. */
74
+ /** Default index-line title length. 100 (reverted from 150 on 2026-08-03:
75
+ * eval #3 §5 showed 100 vs 150 statistically identical; 100 saves ~13% tokens). */
76
+ const DEFAULT_TITLE_CHARS = 100;
77
+ /** Over-fetch multiplier: retrieve `maxItems × 3` candidates so gating + dedup
78
+ * still leave a full menu. Kept at 3 after maxItems 6→5 and the higher floor —
79
+ * permit-short is intended (returning fewer than maxItems when fewer clear the
80
+ * gate is correct, not a defect); raise only if blocks are persistently
81
+ * under-filled in production. */
69
82
  const CANDIDATE_MULTIPLIER = 3;
70
83
  /** First content line, de-markdowned and truncated — the index line title. */
71
- function memoryTitle(content, maxLen = 100) {
84
+ function memoryTitle(content, maxLen = DEFAULT_TITLE_CHARS) {
72
85
  const firstLine = content
73
86
  .split("\n")
74
87
  .map((l) => l.trim())
@@ -88,7 +101,15 @@ function formatDate(iso) {
88
101
  const mm = String(d.getMonth() + 1).padStart(2, "0");
89
102
  return `${dd}.${mm}.${d.getFullYear()}`;
90
103
  }
91
- function formatIndexLine(r) {
104
+ /**
105
+ * Render one production index line. Exported (2026-08-02, relevance eval #v2)
106
+ * so the eval can measure the REAL rendered surface instead of reimplementing
107
+ * it — `maxLen` threads through to `memoryTitle` unchanged (default
108
+ * DEFAULT_TITLE_CHARS = 100, config `recallTitleChars`) so the eval's snippet-length
109
+ * sweep (spec §4.2) can call this SAME function at 100/150/title1sent without
110
+ * duplicating the date/scope/agent/type meta-line logic.
111
+ */
112
+ function formatIndexLine(r, maxLen = DEFAULT_TITLE_CHARS) {
92
113
  // Provenance (#202): date, scope (domain else project), ORIGIN AGENT, type.
93
114
  // The origin agent lets a reader calibrate trust — "from my session" vs
94
115
  // another agent/project — before fetching or acting on an entry.
@@ -100,9 +121,19 @@ function formatIndexLine(r) {
100
121
  ]
101
122
  .filter(Boolean)
102
123
  .join(", ");
103
- return `- [${r.id}] ${memoryTitle(r.content)}${meta ? ` (${meta})` : ""}`;
124
+ return `- [${r.id}] ${memoryTitle(r.content, maxLen)}${meta ? ` (${meta})` : ""}`;
104
125
  }
105
- /** Relevance gate: real text match, or measured cosine above the floor. */
126
+ /**
127
+ * Relevance gate: a real BM25 text match (FTS) passes unconditionally; a
128
+ * vector-only candidate must clear `minSimilarity`.
129
+ *
130
+ * NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
131
+ * weight toward FTS-sourced entries. In practice FTS is currently inert on
132
+ * real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
133
+ * a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
134
+ * is safe as measured. But FTS quality is unmeasured; if FTS starts firing
135
+ * (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
136
+ */
106
137
  function passesRelevanceGate(r, minSimilarity) {
107
138
  if (r.source === "fts" || r.source === "both")
108
139
  return true;
@@ -150,6 +181,7 @@ async function handleRecallIndex(deps, body) {
150
181
  return { status: 200, body: { block: null, skipped: "short-prompt" } };
151
182
  }
152
183
  const maxItems = clampInt(deps.options?.maxItems, DEFAULT_MAX_ITEMS, 1, 20);
184
+ const titleChars = clampInt(deps.options?.titleChars, DEFAULT_TITLE_CHARS, 40, 400);
153
185
  const minSimilarity = clampNumber(deps.options?.minSimilarity, DEFAULT_MIN_SIMILARITY, 0, 1);
154
186
  const turn = deps.registry.beginTurn(sessionId);
155
187
  // Optional client-side scoping (F1 + #203): project + mission_domains (soft
@@ -183,7 +215,7 @@ async function handleRecallIndex(deps, body) {
183
215
  deps.registry.markShown(sessionId, ids);
184
216
  // Exposure signal: shown_count + last_accessed refresh, NOT access_count.
185
217
  storage.touchMemoriesShown(deps.db, ids, new Date().toISOString());
186
- const lines = picked.map((r) => formatIndexLine(r));
218
+ const lines = picked.map((r) => formatIndexLine(r, titleChars));
187
219
  const block = [
188
220
  "## Memory recall (auto)",
189
221
  // Provenance is BUILT IN (owner decision 27.07, option D; extended #202/#204):
@@ -3,5 +3,5 @@
3
3
  * Shared between OC plugin and CC MCP server.
4
4
  */
5
5
  import type Database from "better-sqlite3";
6
- export declare const SEED_LESSON = "## Lesson: 1% Daily Self-Improvement\n\nSelf-improvement protocol: Review your sessions daily. Scan for mistakes, gaps, user corrections, and friction. Identify 1-2 issues maximum. Apply one targeted fix to your agent instruction files (e.g. SOUL.md / MEMORY.md for Hermes profiles, CLAUDE.md for Claude Code). Never overcorrect \u2014 feedback is guidance, not emergency. One fix per issue. Never rewrite entire systems. Before changing something, ask: is this a real pattern or am I reacting to noise?\n\nThe flywheel: Sessions \u2192 Hicortex captures \u2192 Nightly consolidation \u2192 Lessons generated \u2192 Lessons injected into your context \u2192 You apply one fix \u2192 Better sessions tomorrow.\n\nUse hicortex_lessons to check for new insights. If a lesson has high confidence, apply it. If medium, observe for one more day. If nothing needs fixing today, skip \u2014 that's fine.\n\n**Severity:** critical\n**Confidence:** high\n**Generated:** seed-lesson";
6
+ export declare const SEED_LESSON = "1% Daily Self-Improvement\n\nSelf-improvement protocol: Review your sessions daily. Scan for mistakes, gaps, user corrections, and friction. Identify 1-2 issues maximum. Apply one targeted fix to your agent instruction files (e.g. SOUL.md / MEMORY.md for Hermes profiles, CLAUDE.md for Claude Code). Never overcorrect \u2014 feedback is guidance, not emergency. One fix per issue. Never rewrite entire systems. Before changing something, ask: is this a real pattern or am I reacting to noise?\n\nThe flywheel: Sessions \u2192 Hicortex captures \u2192 Nightly consolidation \u2192 Lessons generated \u2192 Lessons injected into your context \u2192 You apply one fix \u2192 Better sessions tomorrow.\n\nUse hicortex_lessons to check for new insights. If a lesson has high confidence, apply it. If medium, observe for one more day. If nothing needs fixing today, skip \u2014 that's fine.\n\n**Severity:** critical\n**Confidence:** high\n**Generated:** seed-lesson";
7
7
  export declare function injectSeedLesson(database: Database.Database, log?: (msg: string) => void): Promise<void>;
@@ -41,7 +41,7 @@ exports.SEED_LESSON = void 0;
41
41
  exports.injectSeedLesson = injectSeedLesson;
42
42
  const embedder_js_1 = require("./embedder.js");
43
43
  const storage = __importStar(require("./storage.js"));
44
- exports.SEED_LESSON = `## Lesson: 1% Daily Self-Improvement
44
+ exports.SEED_LESSON = `1% Daily Self-Improvement
45
45
 
46
46
  Self-improvement protocol: Review your sessions daily. Scan for mistakes, gaps, user corrections, and friction. Identify 1-2 issues maximum. Apply one targeted fix to your agent instruction files (e.g. SOUL.md / MEMORY.md for Hermes profiles, CLAUDE.md for Claude Code). Never overcorrect — feedback is guidance, not emergency. One fix per issue. Never rewrite entire systems. Before changing something, ask: is this a real pattern or am I reacting to noise?
47
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
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": {
@@ -39,6 +39,7 @@
39
39
  "test:watch": "vitest",
40
40
  "eval": "node dist/eval/run-eval.js",
41
41
  "eval:recall-sweep": "node dist/eval/recall-sweep.js",
42
+ "eval:relevance": "node dist/eval/relevance-eval.js",
42
43
  "prepack": "npm run build && rm -rf ./hermes-plugin && mkdir -p ./hermes-plugin && cp -r ../../hermes-plugin/hicortex ./hermes-plugin/ && find ./hermes-plugin -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null || true",
43
44
  "prepublishOnly": "npm run build && rm -rf ./hermes-plugin && mkdir -p ./hermes-plugin && cp -r ../../hermes-plugin/hicortex ./hermes-plugin/ && find ./hermes-plugin -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null || true"
44
45
  },