@gamaze/hicortex 0.16.0 → 0.16.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 +9 -0
- package/dist/capture.d.ts +18 -1
- package/dist/capture.js +3 -2
- package/dist/classify-domains.d.ts +1 -1
- package/dist/classify-domains.js +5 -7
- package/dist/cli.js +10 -2
- package/dist/cluster.d.ts +5 -4
- package/dist/cluster.js +2 -3
- package/dist/consolidate.js +6 -5
- package/dist/db.js +23 -0
- package/dist/dedup.js +1 -1
- package/dist/distiller.js +19 -12
- package/dist/domain-classify.d.ts +1 -1
- package/dist/domain-classify.js +1 -5
- package/dist/eval/relevance-eval.d.ts +64 -0
- package/dist/eval/relevance-eval.js +1954 -0
- package/dist/eval/run-eval.js +0 -1
- package/dist/index.js +3 -3
- package/dist/init.d.ts +165 -0
- package/dist/init.js +283 -57
- package/dist/lessons-context.js +3 -2
- package/dist/mcp-server.js +72 -25
- package/dist/nightly.js +35 -3
- package/dist/nofit.d.ts +1 -1
- package/dist/nofit.js +1 -2
- package/dist/prompts.js +22 -13
- package/dist/recall-index.d.ts +56 -21
- package/dist/recall-index.js +51 -29
- package/dist/retrieval.d.ts +7 -7
- package/dist/retrieval.js +20 -24
- package/dist/schema-prototypes.d.ts +8 -13
- package/dist/schema-prototypes.js +13 -22
- package/dist/seed-lesson.d.ts +1 -1
- package/dist/seed-lesson.js +1 -2
- package/dist/storage.d.ts +9 -12
- package/dist/storage.js +19 -21
- package/dist/types.d.ts +47 -23
- package/domains.example.json +2 -3
- package/hermes-plugin/hicortex/README.md +3 -1
- package/hermes-plugin/hicortex/config.py +33 -2
- package/hermes-plugin/hicortex/plugin.yaml +1 -1
- package/hermes-plugin/hicortex/provider.py +5 -0
- package/package.json +2 -1
package/dist/recall-index.js
CHANGED
|
@@ -55,20 +55,32 @@ 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
|
-
exports.parsePrivacyParam = parsePrivacyParam;
|
|
61
61
|
exports.handleRecallIndex = handleRecallIndex;
|
|
62
62
|
exports.handleMemoryGet = handleMemoryGet;
|
|
63
63
|
exports.formatMemoryGetText = formatMemoryGetText;
|
|
64
64
|
const storage = __importStar(require("./storage.js"));
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
/** Relevance-gate floor for vector-only candidates (config `recallMinSimilarity`).
|
|
66
|
+
* 0.62 (was 0.55; raised 2026-08-03 on the fine-grain floor sweep — see the
|
|
67
|
+
* minSimilarity doc above). */
|
|
68
|
+
const DEFAULT_MIN_SIMILARITY = 0.62;
|
|
69
|
+
/** Max index lines per pushed recall block (config `recallMaxItems`).
|
|
70
|
+
* 5 (was 6; lowered 2026-08-03 — slot 6 is pure padding at floor 0.62). */
|
|
71
|
+
const DEFAULT_MAX_ITEMS = 5;
|
|
67
72
|
const DEFAULT_MIN_PROMPT_LENGTH = 20;
|
|
68
|
-
/**
|
|
73
|
+
/** Default index-line title length. 100 (reverted from 150 on 2026-08-03:
|
|
74
|
+
* eval #3 §5 showed 100 vs 150 statistically identical; 100 saves ~13% tokens). */
|
|
75
|
+
const DEFAULT_TITLE_CHARS = 100;
|
|
76
|
+
/** Over-fetch multiplier: retrieve `maxItems × 3` candidates so gating + dedup
|
|
77
|
+
* still leave a full menu. Kept at 3 after maxItems 6→5 and the higher floor —
|
|
78
|
+
* permit-short is intended (returning fewer than maxItems when fewer clear the
|
|
79
|
+
* gate is correct, not a defect); raise only if blocks are persistently
|
|
80
|
+
* under-filled in production. */
|
|
69
81
|
const CANDIDATE_MULTIPLIER = 3;
|
|
70
82
|
/** First content line, de-markdowned and truncated — the index line title. */
|
|
71
|
-
function memoryTitle(content, maxLen =
|
|
83
|
+
function memoryTitle(content, maxLen = DEFAULT_TITLE_CHARS) {
|
|
72
84
|
const firstLine = content
|
|
73
85
|
.split("\n")
|
|
74
86
|
.map((l) => l.trim())
|
|
@@ -88,7 +100,15 @@ function formatDate(iso) {
|
|
|
88
100
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
89
101
|
return `${dd}.${mm}.${d.getFullYear()}`;
|
|
90
102
|
}
|
|
91
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Render one production index line. Exported (2026-08-02, relevance eval #v2)
|
|
105
|
+
* so the eval can measure the REAL rendered surface instead of reimplementing
|
|
106
|
+
* it — `maxLen` threads through to `memoryTitle` unchanged (default
|
|
107
|
+
* DEFAULT_TITLE_CHARS = 100, config `recallTitleChars`) so the eval's snippet-length
|
|
108
|
+
* sweep (spec §4.2) can call this SAME function at 100/150/title1sent without
|
|
109
|
+
* duplicating the date/scope/agent/type meta-line logic.
|
|
110
|
+
*/
|
|
111
|
+
function formatIndexLine(r, maxLen = DEFAULT_TITLE_CHARS) {
|
|
92
112
|
// Provenance (#202): date, scope (domain else project), ORIGIN AGENT, type.
|
|
93
113
|
// The origin agent lets a reader calibrate trust — "from my session" vs
|
|
94
114
|
// another agent/project — before fetching or acting on an entry.
|
|
@@ -100,9 +120,19 @@ function formatIndexLine(r) {
|
|
|
100
120
|
]
|
|
101
121
|
.filter(Boolean)
|
|
102
122
|
.join(", ");
|
|
103
|
-
return `- [${r.id}] ${memoryTitle(r.content)}${meta ? ` (${meta})` : ""}`;
|
|
123
|
+
return `- [${r.id}] ${memoryTitle(r.content, maxLen)}${meta ? ` (${meta})` : ""}`;
|
|
104
124
|
}
|
|
105
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Relevance gate: a real BM25 text match (FTS) passes unconditionally; a
|
|
127
|
+
* vector-only candidate must clear `minSimilarity`.
|
|
128
|
+
*
|
|
129
|
+
* NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
|
|
130
|
+
* weight toward FTS-sourced entries. In practice FTS is currently inert on
|
|
131
|
+
* real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
|
|
132
|
+
* a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
|
|
133
|
+
* is safe as measured. But FTS quality is unmeasured; if FTS starts firing
|
|
134
|
+
* (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
|
|
135
|
+
*/
|
|
106
136
|
function passesRelevanceGate(r, minSimilarity) {
|
|
107
137
|
if (r.source === "fts" || r.source === "both")
|
|
108
138
|
return true;
|
|
@@ -110,8 +140,8 @@ function passesRelevanceGate(r, minSimilarity) {
|
|
|
110
140
|
}
|
|
111
141
|
/** Normalize a request-supplied string-list param: array of strings or a CSV
|
|
112
142
|
* string → string[] | undefined. Anything else (or an empty result) means
|
|
113
|
-
* "absent" — never a partial guess.
|
|
114
|
-
*
|
|
143
|
+
* "absent" — never a partial guess. Used by `mission_domains` (#203) so it
|
|
144
|
+
* accepts `["A","B"]` and `"A, B"` alike. */
|
|
115
145
|
function parseStringListParam(v) {
|
|
116
146
|
const items = Array.isArray(v)
|
|
117
147
|
? v.filter((x) => typeof x === "string")
|
|
@@ -121,13 +151,6 @@ function parseStringListParam(v) {
|
|
|
121
151
|
const cleaned = items.map((s) => s.trim()).filter(Boolean);
|
|
122
152
|
return cleaned.length > 0 ? cleaned : undefined;
|
|
123
153
|
}
|
|
124
|
-
/** Normalize a request-supplied privacy filter: array of strings or a CSV
|
|
125
|
-
* string → string[] | undefined. Anything else (or an empty result) means
|
|
126
|
-
* "no filter" — never a partial guess. Delegates to parseStringListParam;
|
|
127
|
-
* kept as a named export for tests and handleMemoryGet callers. */
|
|
128
|
-
function parsePrivacyParam(v) {
|
|
129
|
-
return parseStringListParam(v);
|
|
130
|
-
}
|
|
131
154
|
/**
|
|
132
155
|
* Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
|
|
133
156
|
* all behavior lives here so tests exercise it directly.
|
|
@@ -150,16 +173,17 @@ async function handleRecallIndex(deps, body) {
|
|
|
150
173
|
return { status: 200, body: { block: null, skipped: "short-prompt" } };
|
|
151
174
|
}
|
|
152
175
|
const maxItems = clampInt(deps.options?.maxItems, DEFAULT_MAX_ITEMS, 1, 20);
|
|
176
|
+
const titleChars = clampInt(deps.options?.titleChars, DEFAULT_TITLE_CHARS, 40, 400);
|
|
153
177
|
const minSimilarity = clampNumber(deps.options?.minSimilarity, DEFAULT_MIN_SIMILARITY, 0, 1);
|
|
154
178
|
const turn = deps.registry.beginTurn(sessionId);
|
|
155
179
|
// Optional client-side scoping (F1 + #203): project + mission_domains (soft
|
|
156
|
-
// affinity)
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
180
|
+
// affinity) ride the body and are pushed into retrieval. project is cwd-
|
|
181
|
+
// derived (CC/OC) or gateway-supplied; mission_domains is Hermes-declared
|
|
182
|
+
// (plugin config). Neither excludes anything — both are zero-boost-neutral
|
|
183
|
+
// score terms in computeScore. (0.16.x: `privacy` is no longer threaded —
|
|
184
|
+
// vestigial column, never filtered; a plugin's privacy_filter is a no-op.)
|
|
160
185
|
const filters = {
|
|
161
186
|
project: typeof req.project === "string" && req.project ? req.project : undefined,
|
|
162
|
-
privacy: parsePrivacyParam(req.privacy),
|
|
163
187
|
mission_domains: parseStringListParam(req.mission_domains),
|
|
164
188
|
};
|
|
165
189
|
let results;
|
|
@@ -183,7 +207,7 @@ async function handleRecallIndex(deps, body) {
|
|
|
183
207
|
deps.registry.markShown(sessionId, ids);
|
|
184
208
|
// Exposure signal: shown_count + last_accessed refresh, NOT access_count.
|
|
185
209
|
storage.touchMemoriesShown(deps.db, ids, new Date().toISOString());
|
|
186
|
-
const lines = picked.map((r) => formatIndexLine(r));
|
|
210
|
+
const lines = picked.map((r) => formatIndexLine(r, titleChars));
|
|
187
211
|
const block = [
|
|
188
212
|
"## Memory recall (auto)",
|
|
189
213
|
// Provenance is BUILT IN (owner decision 27.07, option D; extended #202/#204):
|
|
@@ -206,10 +230,11 @@ async function handleRecallIndex(deps, body) {
|
|
|
206
230
|
*
|
|
207
231
|
* - Short/prefix ids resolve via storage.resolveMemoryId (F6) — the 8-char
|
|
208
232
|
* citation ids agents are taught must work here like on /update, /delete.
|
|
209
|
-
* - Optional `privacy` filter (array or CSV): when present and the memory's
|
|
210
|
-
* privacy level is not in the allowed set, respond 404 with the SAME
|
|
211
|
-
* not-found message — a scoped client must not learn the memory exists.
|
|
212
233
|
* - A successful fetch is real use: access_count + 1 (strengthen).
|
|
234
|
+
*
|
|
235
|
+
* 0.16.x: the `privacy` filter gate was removed — the column is vestigial and
|
|
236
|
+
* never filtered. Callers may still send a `privacy` field (backward compat)
|
|
237
|
+
* but it is ignored.
|
|
213
238
|
*/
|
|
214
239
|
function handleMemoryGet(db, query) {
|
|
215
240
|
const id = typeof query.id === "string" ? query.id : "";
|
|
@@ -225,9 +250,6 @@ function handleMemoryGet(db, query) {
|
|
|
225
250
|
const mem = storage.getMemory(db, fullId);
|
|
226
251
|
if (!mem)
|
|
227
252
|
return notFound;
|
|
228
|
-
const privacy = parsePrivacyParam(query.privacy);
|
|
229
|
-
if (privacy && !privacy.includes(mem.privacy))
|
|
230
|
-
return notFound;
|
|
231
253
|
storage.strengthenMemory(db, fullId, new Date().toISOString());
|
|
232
254
|
// `citation` is server-rendered so every plugin surfaces the same built-in
|
|
233
255
|
// provenance norm (owner directive 27.07) — see #193.
|
package/dist/retrieval.d.ts
CHANGED
|
@@ -178,10 +178,12 @@ export interface EmbedFn {
|
|
|
178
178
|
*
|
|
179
179
|
* #203 retrieval scoping: `project` and `missionDomains` are SOFT affinity
|
|
180
180
|
* terms in computeScore (zero-boost neutral, never a penalty), NOT filters.
|
|
181
|
-
* `privacy`
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
181
|
+
* `privacy` is NOT a filter (0.16.x: the column is fully vestigial — stored,
|
|
182
|
+
* never filtered; the distiller no longer sets it and retrieval ignores it
|
|
183
|
+
* entirely). `sourceAgent` remains a hard filter (kept for completeness; no
|
|
184
|
+
* production caller currently passes it). When neither project nor
|
|
185
|
+
* missionDomains is sent, scoring is byte-identical to pre-#203 — the
|
|
186
|
+
* kill-switch / no-op guarantee.
|
|
185
187
|
*/
|
|
186
188
|
export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query: string, options?: {
|
|
187
189
|
limit?: number;
|
|
@@ -189,7 +191,6 @@ export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query:
|
|
|
189
191
|
* Formerly a hard WHERE filter (#192); softening removes cross-scope
|
|
190
192
|
* starvation without excluding anything. */
|
|
191
193
|
project?: string | null;
|
|
192
|
-
privacy?: string[];
|
|
193
194
|
sourceAgent?: string;
|
|
194
195
|
/** #203: Hermes mission domains (declared in plugin config). Soft domain
|
|
195
196
|
* affinity in computeScore via max overlapping memory_tags.weight. */
|
|
@@ -206,11 +207,10 @@ export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query:
|
|
|
206
207
|
queryEmbedding?: Float32Array;
|
|
207
208
|
}): Promise<MemorySearchResult[]>;
|
|
208
209
|
/**
|
|
209
|
-
* Get recent context, optionally filtered by project
|
|
210
|
+
* Get recent context, optionally filtered by project.
|
|
210
211
|
*/
|
|
211
212
|
export declare function searchRecent(db: Database.Database, options?: {
|
|
212
213
|
project?: string | null;
|
|
213
214
|
limit?: number;
|
|
214
|
-
privacy?: string[];
|
|
215
215
|
}): MemorySearchResult[];
|
|
216
216
|
export {};
|
package/dist/retrieval.js
CHANGED
|
@@ -510,15 +510,16 @@ function reciprocalRankFusion(rankedLists, k = DEFAULT_RRF_K) {
|
|
|
510
510
|
*
|
|
511
511
|
* #203 retrieval scoping: `project` and `missionDomains` are SOFT affinity
|
|
512
512
|
* terms in computeScore (zero-boost neutral, never a penalty), NOT filters.
|
|
513
|
-
* `privacy`
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
513
|
+
* `privacy` is NOT a filter (0.16.x: the column is fully vestigial — stored,
|
|
514
|
+
* never filtered; the distiller no longer sets it and retrieval ignores it
|
|
515
|
+
* entirely). `sourceAgent` remains a hard filter (kept for completeness; no
|
|
516
|
+
* production caller currently passes it). When neither project nor
|
|
517
|
+
* missionDomains is sent, scoring is byte-identical to pre-#203 — the
|
|
518
|
+
* kill-switch / no-op guarantee.
|
|
517
519
|
*/
|
|
518
520
|
async function retrieve(db, embedFn, query, options) {
|
|
519
521
|
const limit = options?.limit ?? recallDefaults.searchLimit;
|
|
520
522
|
const project = options?.project;
|
|
521
|
-
const privacy = options?.privacy;
|
|
522
523
|
const sourceAgent = options?.sourceAgent;
|
|
523
524
|
const missionDomains = options?.missionDomains;
|
|
524
525
|
const now = new Date();
|
|
@@ -534,15 +535,17 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
534
535
|
// over-fetch — the old flat limit*3 intersected a global top-15 with (for the
|
|
535
536
|
// median project) ~1% of the corpus, starving every filtered query.
|
|
536
537
|
// #203: project is NO LONGER a filter (soft affinity now), so it does not
|
|
537
|
-
// trigger over-fetch;
|
|
538
|
-
|
|
538
|
+
// trigger over-fetch; only sourceAgent still does (it remains a hard filter).
|
|
539
|
+
// 0.16.x: privacy is no longer a filter either (column is vestigial).
|
|
540
|
+
const filtered = Boolean(sourceAgent);
|
|
539
541
|
const fetchLimit = filtered ? Math.min(limit * 20, 200) : limit * 3;
|
|
540
542
|
let vecCandidates = storage.vectorSearch(db, queryEmbedding, fetchLimit, []);
|
|
541
543
|
let ftsCandidates = [];
|
|
542
544
|
try {
|
|
543
|
-
//
|
|
544
|
-
// is
|
|
545
|
-
|
|
545
|
+
// sourceAgent is pushed into the FTS SQL (hard filter). project is NOT (it
|
|
546
|
+
// is a soft affinity boost in computeScore as of #203). privacy is NOT
|
|
547
|
+
// (0.16.x: vestigial column, never filtered).
|
|
548
|
+
ftsCandidates = storage.searchFts(db, query, fetchLimit, sourceAgent);
|
|
546
549
|
}
|
|
547
550
|
catch {
|
|
548
551
|
// FTS5 search can fail on special characters; fall back to vector-only
|
|
@@ -550,12 +553,9 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
550
553
|
if (vecCandidates.length === 0 && ftsCandidates.length === 0) {
|
|
551
554
|
return [];
|
|
552
555
|
}
|
|
553
|
-
// Post-filter vector candidates (sqlite-vec can't filter).
|
|
554
|
-
// hard filter (
|
|
555
|
-
//
|
|
556
|
-
if (privacy) {
|
|
557
|
-
vecCandidates = vecCandidates.filter((c) => privacy.includes(c.privacy));
|
|
558
|
-
}
|
|
556
|
+
// Post-filter vector candidates (sqlite-vec can't filter). sourceAgent stays
|
|
557
|
+
// a hard filter (see options doc); project is scored not filtered (#203);
|
|
558
|
+
// privacy is no longer filtered (0.16.x — vestigial).
|
|
559
559
|
if (sourceAgent) {
|
|
560
560
|
vecCandidates = vecCandidates.filter((c) => c.source_agent === sourceAgent);
|
|
561
561
|
}
|
|
@@ -593,9 +593,8 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
593
593
|
if (!mem)
|
|
594
594
|
continue;
|
|
595
595
|
// #203: project check removed — project is a soft affinity in computeScore,
|
|
596
|
-
// not a filter. privacy
|
|
597
|
-
|
|
598
|
-
continue;
|
|
596
|
+
// not a filter. 0.16.x: privacy check removed — the column is vestigial,
|
|
597
|
+
// never filtered. sourceAgent stays a hard filter.
|
|
599
598
|
if (sourceAgent && mem.source_agent !== sourceAgent)
|
|
600
599
|
continue;
|
|
601
600
|
candidateMap.set(gid, { mem, distance: DEFAULT_GRAPH_DISTANCE, source: "graph" });
|
|
@@ -677,12 +676,11 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
677
676
|
return results;
|
|
678
677
|
}
|
|
679
678
|
/**
|
|
680
|
-
* Get recent context, optionally filtered by project
|
|
679
|
+
* Get recent context, optionally filtered by project.
|
|
681
680
|
*/
|
|
682
681
|
function searchRecent(db, options) {
|
|
683
682
|
const limit = options?.limit ?? recallDefaults.recentLimit;
|
|
684
683
|
const project = options?.project;
|
|
685
|
-
const privacy = options?.privacy;
|
|
686
684
|
const now = new Date();
|
|
687
685
|
// #192 breadth: 30 → 180-day default window (config recentWindowDays).
|
|
688
686
|
// "Recent" for a long-lived corpus is a season, not a month; the narrow
|
|
@@ -691,9 +689,7 @@ function searchRecent(db, options) {
|
|
|
691
689
|
if (project) {
|
|
692
690
|
candidates = candidates.filter((c) => c.project === project);
|
|
693
691
|
}
|
|
694
|
-
|
|
695
|
-
candidates = candidates.filter((c) => privacy.includes(c.privacy));
|
|
696
|
-
}
|
|
692
|
+
// 0.16.x: privacy filter removed — the column is vestigial, never filtered.
|
|
697
693
|
if (candidates.length === 0)
|
|
698
694
|
return [];
|
|
699
695
|
const allIds = candidates.map((c) => c.id);
|
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
* embedding of the domain's config description instead.
|
|
12
12
|
* - weight(memory, tag) = cosine(memory embedding, prototype(tag)). Both
|
|
13
13
|
* vectors are L2-normalized, so cosine reduces to a dot product.
|
|
14
|
-
* - PRIMARY (memories.domain) = argmax-weight tag,
|
|
15
|
-
*
|
|
16
|
-
* the owner's Work firewall). Fully mechanical, no LLM.
|
|
14
|
+
* - PRIMARY (memories.domain) = argmax-weight tag, with LLM tag order
|
|
15
|
+
* breaking exact-weight ties. Fully mechanical, no LLM.
|
|
17
16
|
*
|
|
18
17
|
* The LLM decides ONLY the discrete part (which schemas apply — see
|
|
19
18
|
* domain-classify.ts); ALL gradation is derived from embeddings here.
|
|
@@ -65,24 +64,20 @@ export interface WeightedTag {
|
|
|
65
64
|
tag: string;
|
|
66
65
|
weight: number | null;
|
|
67
66
|
}
|
|
68
|
-
/** The configured compartment domain names (DomainDef.compartment === true). */
|
|
69
|
-
export declare function compartmentSet(domains: DomainDef[]): Set<string>;
|
|
70
67
|
/**
|
|
71
68
|
* Derive the PRIMARY tag (memories.domain) from a weighted tag set.
|
|
72
69
|
*
|
|
73
70
|
* Rules (deterministic, no LLM):
|
|
74
|
-
* 1.
|
|
75
|
-
* (unusual) case of several arises.
|
|
76
|
-
* 2. Else the argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
71
|
+
* 1. The argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
77
72
|
* order: ties (and all-null weights) resolve to the EARLIEST array
|
|
78
73
|
* position — strict `>` comparison keeps the first maximum.
|
|
79
|
-
*
|
|
74
|
+
* 2. A null weight loses to any numeric weight (treated as -Infinity).
|
|
80
75
|
*
|
|
81
76
|
* Throws on an empty tag set — callers guarantee >= 1 tag (an empty tag set
|
|
82
77
|
* from the classifier is a NO-FIT and must be routed through nofit.ts, never
|
|
83
78
|
* here); an empty set reaching this function is a programming error.
|
|
84
79
|
*/
|
|
85
|
-
export declare function derivePrimary(tags: WeightedTag[]
|
|
80
|
+
export declare function derivePrimary(tags: WeightedTag[]): string;
|
|
86
81
|
export interface PrototypeStat {
|
|
87
82
|
domain: string;
|
|
88
83
|
memberCount: number;
|
|
@@ -151,9 +146,9 @@ export declare function recomputeAllTagWeights(db: Database.Database, prototypes
|
|
|
151
146
|
};
|
|
152
147
|
/**
|
|
153
148
|
* Re-derive the PRIMARY (memories.domain) of every tagged memory from its
|
|
154
|
-
* current tag weights:
|
|
155
|
-
*
|
|
156
|
-
*
|
|
149
|
+
* current tag weights: argmax weight, LLM order (memory_tags insertion order =
|
|
150
|
+
* rowid, written most-relevant-first by storage.setMemoryTags) breaking
|
|
151
|
+
* exact-weight ties.
|
|
157
152
|
*
|
|
158
153
|
* Memories with NO memory_tags rows are untouched (e.g. infra-skipped rows
|
|
159
154
|
* awaiting classification — issue #150 discipline).
|
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* embedding of the domain's config description instead.
|
|
13
13
|
* - weight(memory, tag) = cosine(memory embedding, prototype(tag)). Both
|
|
14
14
|
* vectors are L2-normalized, so cosine reduces to a dot product.
|
|
15
|
-
* - PRIMARY (memories.domain) = argmax-weight tag,
|
|
16
|
-
*
|
|
17
|
-
* the owner's Work firewall). Fully mechanical, no LLM.
|
|
15
|
+
* - PRIMARY (memories.domain) = argmax-weight tag, with LLM tag order
|
|
16
|
+
* breaking exact-weight ties. Fully mechanical, no LLM.
|
|
18
17
|
*
|
|
19
18
|
* The LLM decides ONLY the discrete part (which schemas apply — see
|
|
20
19
|
* domain-classify.ts); ALL gradation is derived from embeddings here.
|
|
@@ -30,7 +29,6 @@ exports.blobToVec = blobToVec;
|
|
|
30
29
|
exports.l2Normalize = l2Normalize;
|
|
31
30
|
exports.weightedAdd = weightedAdd;
|
|
32
31
|
exports.tagWeight = tagWeight;
|
|
33
|
-
exports.compartmentSet = compartmentSet;
|
|
34
32
|
exports.derivePrimary = derivePrimary;
|
|
35
33
|
exports.loadDomainPrototypes = loadDomainPrototypes;
|
|
36
34
|
exports.computeDomainPrototypes = computeDomainPrototypes;
|
|
@@ -107,33 +105,23 @@ function tagWeight(memoryEmbedding, prototype) {
|
|
|
107
105
|
dot += memoryEmbedding[i] * prototype[i];
|
|
108
106
|
return dot;
|
|
109
107
|
}
|
|
110
|
-
/** The configured compartment domain names (DomainDef.compartment === true). */
|
|
111
|
-
function compartmentSet(domains) {
|
|
112
|
-
return new Set(domains.filter((d) => d.compartment === true).map((d) => d.name));
|
|
113
|
-
}
|
|
114
108
|
/**
|
|
115
109
|
* Derive the PRIMARY tag (memories.domain) from a weighted tag set.
|
|
116
110
|
*
|
|
117
111
|
* Rules (deterministic, no LLM):
|
|
118
|
-
* 1.
|
|
119
|
-
* (unusual) case of several arises.
|
|
120
|
-
* 2. Else the argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
112
|
+
* 1. The argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
121
113
|
* order: ties (and all-null weights) resolve to the EARLIEST array
|
|
122
114
|
* position — strict `>` comparison keeps the first maximum.
|
|
123
|
-
*
|
|
115
|
+
* 2. A null weight loses to any numeric weight (treated as -Infinity).
|
|
124
116
|
*
|
|
125
117
|
* Throws on an empty tag set — callers guarantee >= 1 tag (an empty tag set
|
|
126
118
|
* from the classifier is a NO-FIT and must be routed through nofit.ts, never
|
|
127
119
|
* here); an empty set reaching this function is a programming error.
|
|
128
120
|
*/
|
|
129
|
-
function derivePrimary(tags
|
|
121
|
+
function derivePrimary(tags) {
|
|
130
122
|
if (tags.length === 0) {
|
|
131
123
|
throw new Error("derivePrimary: empty tag set (callers must pass >= 1 tag)");
|
|
132
124
|
}
|
|
133
|
-
for (const t of tags) {
|
|
134
|
-
if (compartments.has(t.tag))
|
|
135
|
-
return t.tag;
|
|
136
|
-
}
|
|
137
125
|
let best = tags[0];
|
|
138
126
|
let bestWeight = best.weight ?? Number.NEGATIVE_INFINITY;
|
|
139
127
|
for (let i = 1; i < tags.length; i++) {
|
|
@@ -313,15 +301,18 @@ function recomputeAllTagWeights(db, prototypes) {
|
|
|
313
301
|
}
|
|
314
302
|
/**
|
|
315
303
|
* Re-derive the PRIMARY (memories.domain) of every tagged memory from its
|
|
316
|
-
* current tag weights:
|
|
317
|
-
*
|
|
318
|
-
*
|
|
304
|
+
* current tag weights: argmax weight, LLM order (memory_tags insertion order =
|
|
305
|
+
* rowid, written most-relevant-first by storage.setMemoryTags) breaking
|
|
306
|
+
* exact-weight ties.
|
|
319
307
|
*
|
|
320
308
|
* Memories with NO memory_tags rows are untouched (e.g. infra-skipped rows
|
|
321
309
|
* awaiting classification — issue #150 discipline).
|
|
322
310
|
*/
|
|
323
311
|
function refreshPrimaries(db, domains) {
|
|
324
|
-
|
|
312
|
+
// `domains` is accepted for API symmetry with the other reconsolidation
|
|
313
|
+
// passes (which need prototypes/weights); the primary is now pure argmax
|
|
314
|
+
// and does not depend on the domain set.
|
|
315
|
+
void domains;
|
|
325
316
|
const rows = db
|
|
326
317
|
.prepare(`SELECT mt.memory_id, mt.tag, mt.weight, m.domain
|
|
327
318
|
FROM memory_tags mt JOIN memories m ON m.id = mt.memory_id
|
|
@@ -341,7 +332,7 @@ function refreshPrimaries(db, domains) {
|
|
|
341
332
|
let updated = 0;
|
|
342
333
|
const tx = db.transaction(() => {
|
|
343
334
|
for (const [memoryId, entry] of byMemory) {
|
|
344
|
-
const primary = derivePrimary(entry.tags
|
|
335
|
+
const primary = derivePrimary(entry.tags);
|
|
345
336
|
if (primary !== entry.domain) {
|
|
346
337
|
update.run(primary, memoryId);
|
|
347
338
|
updated++;
|
package/dist/seed-lesson.d.ts
CHANGED
|
@@ -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 = "
|
|
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>;
|
package/dist/seed-lesson.js
CHANGED
|
@@ -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 =
|
|
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
|
|
|
@@ -65,7 +65,6 @@ async function injectSeedLesson(database, log = console.log) {
|
|
|
65
65
|
project: "global",
|
|
66
66
|
memoryType: "lesson",
|
|
67
67
|
baseStrength: 0.95,
|
|
68
|
-
privacy: "WORK",
|
|
69
68
|
});
|
|
70
69
|
log("[hicortex] Seed lesson injected: Daily Self-Improvement Protocol");
|
|
71
70
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -54,11 +54,6 @@ export interface SetMemoryTagsOptions {
|
|
|
54
54
|
* (repaired by the next nightly recompute).
|
|
55
55
|
*/
|
|
56
56
|
weights?: Record<string, number | null>;
|
|
57
|
-
/**
|
|
58
|
-
* Compartment domain names (DomainDef.compartment === true): a tagged
|
|
59
|
-
* compartment domain becomes the primary regardless of weights.
|
|
60
|
-
*/
|
|
61
|
-
compartments?: Set<string>;
|
|
62
57
|
}
|
|
63
58
|
/**
|
|
64
59
|
* Set a memory's classification tags (graded schema model).
|
|
@@ -69,9 +64,8 @@ export interface SetMemoryTagsOptions {
|
|
|
69
64
|
* row stores its association weight (NULL when not yet computed).
|
|
70
65
|
*
|
|
71
66
|
* The PRIMARY (memories.domain) is DERIVED here — never passed in by the LLM:
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* weights never diverge.
|
|
67
|
+
* argmax weight, else first tag (all-null weights). The whole update is one
|
|
68
|
+
* transaction so domain, tag set, and weights never diverge.
|
|
75
69
|
*
|
|
76
70
|
* @returns the derived primary written to memories.domain
|
|
77
71
|
*/
|
|
@@ -149,9 +143,10 @@ export declare function getBm25Weights(): Bm25Weights;
|
|
|
149
143
|
*
|
|
150
144
|
* `project` is NOT a filter here (#203): the hard project WHERE from #192 was
|
|
151
145
|
* removed — project is now a soft affinity boost in retrieval.computeScore AND
|
|
152
|
-
* a weighted field in BM25F (#205). `privacy`
|
|
153
|
-
*
|
|
154
|
-
*
|
|
146
|
+
* a weighted field in BM25F (#205). `privacy` is NOT a filter (0.16.x: the
|
|
147
|
+
* column is fully vestigial — stored, never filtered; the privacy IN-clause
|
|
148
|
+
* was removed). `sourceAgent` stays a hard filter (kept for completeness; no
|
|
149
|
+
* production caller of retrieve() currently passes it).
|
|
155
150
|
*
|
|
156
151
|
* #205 sign handling: FTS5's `bm25(table, w0, w1, …)` returns a NEGATIVE score
|
|
157
152
|
* where MORE-negative = better match (it is 1 − the normalized BM25 score,
|
|
@@ -162,7 +157,7 @@ export declare function getBm25Weights(): Bm25Weights;
|
|
|
162
157
|
* positionally as parameters (NOT string-interpolated) so query-planner
|
|
163
158
|
* caching is unaffected and the config path is the only editor.
|
|
164
159
|
*/
|
|
165
|
-
export declare function searchFts(db: Database.Database, query: string, limit?: number,
|
|
160
|
+
export declare function searchFts(db: Database.Database, query: string, limit?: number, sourceAgent?: string): Array<Memory & {
|
|
166
161
|
rank: number;
|
|
167
162
|
}>;
|
|
168
163
|
/**
|
|
@@ -184,6 +179,8 @@ export declare function insertMemoriesBatch(db: Database.Database, memories: Arr
|
|
|
184
179
|
content: string;
|
|
185
180
|
embedding: Float32Array;
|
|
186
181
|
sourceAgent?: string;
|
|
182
|
+
sourceAgentId?: string | null;
|
|
183
|
+
sourceDomain?: string | null;
|
|
187
184
|
sourceSession?: string | null;
|
|
188
185
|
project?: string | null;
|
|
189
186
|
privacy?: string;
|
package/dist/storage.js
CHANGED
|
@@ -69,10 +69,10 @@ function insertMemory(db, content, embedding, opts = {}) {
|
|
|
69
69
|
const result = db
|
|
70
70
|
.prepare(`INSERT OR IGNORE INTO memories
|
|
71
71
|
(id, content, base_strength, last_accessed, access_count,
|
|
72
|
-
created_at, ingested_at, source_agent,
|
|
73
|
-
privacy, memory_type)
|
|
74
|
-
VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?)`)
|
|
75
|
-
.run(id, content, opts.baseStrength ?? 0.5, ts, ts, ingestedTs, opts.sourceAgent ?? "default", sourceSession, opts.project ?? null, opts.privacy ??
|
|
72
|
+
created_at, ingested_at, source_agent, source_agent_id, source_session,
|
|
73
|
+
source_domain, project, privacy, memory_type)
|
|
74
|
+
VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
75
|
+
.run(id, content, opts.baseStrength ?? 0.5, ts, ts, ingestedTs, opts.sourceAgent ?? "default", opts.sourceAgentId ?? null, sourceSession, opts.sourceDomain ?? null, opts.project ?? null, opts.privacy ?? null, opts.memoryType ?? "episode");
|
|
76
76
|
if (result.changes > 0) {
|
|
77
77
|
// New row — store its vector.
|
|
78
78
|
db.prepare("INSERT INTO memory_vectors (id, embedding) VALUES (?, ?)").run(id, embedToBlob(embedding));
|
|
@@ -192,9 +192,8 @@ function deleteMemory(db, memoryId) {
|
|
|
192
192
|
* row stores its association weight (NULL when not yet computed).
|
|
193
193
|
*
|
|
194
194
|
* The PRIMARY (memories.domain) is DERIVED here — never passed in by the LLM:
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* weights never diverge.
|
|
195
|
+
* argmax weight, else first tag (all-null weights). The whole update is one
|
|
196
|
+
* transaction so domain, tag set, and weights never diverge.
|
|
198
197
|
*
|
|
199
198
|
* @returns the derived primary written to memories.domain
|
|
200
199
|
*/
|
|
@@ -209,7 +208,7 @@ function setMemoryTags(db, memoryId, tags, options = {}) {
|
|
|
209
208
|
tag,
|
|
210
209
|
weight: options.weights?.[tag] ?? null,
|
|
211
210
|
}));
|
|
212
|
-
const primary = (0, schema_prototypes_js_1.derivePrimary)(weighted
|
|
211
|
+
const primary = (0, schema_prototypes_js_1.derivePrimary)(weighted);
|
|
213
212
|
const setDomain = db.prepare("UPDATE memories SET domain = ? WHERE id = ?");
|
|
214
213
|
const clearTags = db.prepare("DELETE FROM memory_tags WHERE memory_id = ?");
|
|
215
214
|
const insertTag = db.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag, weight) VALUES (?, ?, ?)");
|
|
@@ -352,9 +351,10 @@ function getBm25Weights() {
|
|
|
352
351
|
*
|
|
353
352
|
* `project` is NOT a filter here (#203): the hard project WHERE from #192 was
|
|
354
353
|
* removed — project is now a soft affinity boost in retrieval.computeScore AND
|
|
355
|
-
* a weighted field in BM25F (#205). `privacy`
|
|
356
|
-
*
|
|
357
|
-
*
|
|
354
|
+
* a weighted field in BM25F (#205). `privacy` is NOT a filter (0.16.x: the
|
|
355
|
+
* column is fully vestigial — stored, never filtered; the privacy IN-clause
|
|
356
|
+
* was removed). `sourceAgent` stays a hard filter (kept for completeness; no
|
|
357
|
+
* production caller of retrieve() currently passes it).
|
|
358
358
|
*
|
|
359
359
|
* #205 sign handling: FTS5's `bm25(table, w0, w1, …)` returns a NEGATIVE score
|
|
360
360
|
* where MORE-negative = better match (it is 1 − the normalized BM25 score,
|
|
@@ -365,14 +365,9 @@ function getBm25Weights() {
|
|
|
365
365
|
* positionally as parameters (NOT string-interpolated) so query-planner
|
|
366
366
|
* caching is unaffected and the config path is the only editor.
|
|
367
367
|
*/
|
|
368
|
-
function searchFts(db, query, limit = 10,
|
|
368
|
+
function searchFts(db, query, limit = 10, sourceAgent) {
|
|
369
369
|
const conditions = ["memories_fts MATCH ?"];
|
|
370
370
|
const params = [query];
|
|
371
|
-
if (privacy && privacy.length > 0) {
|
|
372
|
-
const placeholders = privacy.map(() => "?").join(", ");
|
|
373
|
-
conditions.push(`m.privacy IN (${placeholders})`);
|
|
374
|
-
params.push(...privacy);
|
|
375
|
-
}
|
|
376
371
|
if (sourceAgent) {
|
|
377
372
|
conditions.push("m.source_agent = ?");
|
|
378
373
|
params.push(sourceAgent);
|
|
@@ -460,18 +455,21 @@ function deleteLinks(db, memoryId) {
|
|
|
460
455
|
* Batch insert memories. Returns count inserted.
|
|
461
456
|
*/
|
|
462
457
|
function insertMemoriesBatch(db, memories) {
|
|
458
|
+
// privacy default is null (0.16.x: the distiller no longer sets WORK — the
|
|
459
|
+
// column is vestigial, never filtered, and goes NULL unless a caller sends
|
|
460
|
+
// an explicit value).
|
|
463
461
|
const insertMem = db.prepare(`INSERT INTO memories
|
|
464
462
|
(id, content, base_strength, last_accessed, access_count,
|
|
465
|
-
created_at, ingested_at, source_agent,
|
|
466
|
-
privacy, memory_type)
|
|
467
|
-
VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?)`);
|
|
463
|
+
created_at, ingested_at, source_agent, source_agent_id, source_session,
|
|
464
|
+
source_domain, project, privacy, memory_type)
|
|
465
|
+
VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
468
466
|
const insertVec = db.prepare("INSERT INTO memory_vectors (id, embedding) VALUES (?, ?)");
|
|
469
467
|
const tx = db.transaction(() => {
|
|
470
468
|
let count = 0;
|
|
471
469
|
for (const mem of memories) {
|
|
472
470
|
const id = (0, node_crypto_1.randomUUID)();
|
|
473
471
|
const ts = nowIso();
|
|
474
|
-
insertMem.run(id, mem.content, mem.baseStrength ?? 0.5, ts, ts, ts, mem.sourceAgent ?? "default", mem.sourceSession ?? null, mem.project ?? null, mem.privacy ??
|
|
472
|
+
insertMem.run(id, mem.content, mem.baseStrength ?? 0.5, ts, ts, ts, mem.sourceAgent ?? "default", mem.sourceAgentId ?? null, mem.sourceSession ?? null, mem.sourceDomain ?? null, mem.project ?? null, mem.privacy ?? null, mem.memoryType ?? "episode");
|
|
475
473
|
insertVec.run(id, embedToBlob(mem.embedding));
|
|
476
474
|
count++;
|
|
477
475
|
}
|