@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.
Files changed (43) hide show
  1. package/README.md +9 -0
  2. package/dist/capture.d.ts +18 -1
  3. package/dist/capture.js +3 -2
  4. package/dist/classify-domains.d.ts +1 -1
  5. package/dist/classify-domains.js +5 -7
  6. package/dist/cli.js +10 -2
  7. package/dist/cluster.d.ts +5 -4
  8. package/dist/cluster.js +2 -3
  9. package/dist/consolidate.js +6 -5
  10. package/dist/db.js +23 -0
  11. package/dist/dedup.js +1 -1
  12. package/dist/distiller.js +19 -12
  13. package/dist/domain-classify.d.ts +1 -1
  14. package/dist/domain-classify.js +1 -5
  15. package/dist/eval/relevance-eval.d.ts +64 -0
  16. package/dist/eval/relevance-eval.js +1954 -0
  17. package/dist/eval/run-eval.js +0 -1
  18. package/dist/index.js +3 -3
  19. package/dist/init.d.ts +165 -0
  20. package/dist/init.js +283 -57
  21. package/dist/lessons-context.js +3 -2
  22. package/dist/mcp-server.js +72 -25
  23. package/dist/nightly.js +35 -3
  24. package/dist/nofit.d.ts +1 -1
  25. package/dist/nofit.js +1 -2
  26. package/dist/prompts.js +22 -13
  27. package/dist/recall-index.d.ts +56 -21
  28. package/dist/recall-index.js +51 -29
  29. package/dist/retrieval.d.ts +7 -7
  30. package/dist/retrieval.js +20 -24
  31. package/dist/schema-prototypes.d.ts +8 -13
  32. package/dist/schema-prototypes.js +13 -22
  33. package/dist/seed-lesson.d.ts +1 -1
  34. package/dist/seed-lesson.js +1 -2
  35. package/dist/storage.d.ts +9 -12
  36. package/dist/storage.js +19 -21
  37. package/dist/types.d.ts +47 -23
  38. package/domains.example.json +2 -3
  39. package/hermes-plugin/hicortex/README.md +3 -1
  40. package/hermes-plugin/hicortex/config.py +33 -2
  41. package/hermes-plugin/hicortex/plugin.yaml +1 -1
  42. package/hermes-plugin/hicortex/provider.py +5 -0
  43. package/package.json +2 -1
@@ -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
- const DEFAULT_MIN_SIMILARITY = 0.55;
66
- const DEFAULT_MAX_ITEMS = 6;
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
- /** Retrieve more than maxItems so gating + dedup still leave a full menu. */
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 = 100) {
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
- function formatIndexLine(r) {
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
- /** Relevance gate: real text match, or measured cosine above the floor. */
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. Shared by `parsePrivacyParam` and
114
- * `mission_domains` (#203) so both accept `["A","B"]` and `"A, B"` alike. */
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) and privacy (hard filter) ride the body and are pushed into
157
- // retrieval. project is cwd-derived (CC/OC) or gateway-supplied; mission_domains
158
- // is Hermes-declared (plugin config). Neither excludes anything — both are
159
- // zero-boost-neutral score terms in computeScore.
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.
@@ -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` remains a hard filter (security boundary). `sourceAgent` remains a
182
- * hard filter (kept for completeness; no production caller currently passes
183
- * it). When neither project nor missionDomains is sent, scoring is byte-
184
- * identical to pre-#203 the kill-switch / no-op guarantee.
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 and privacy.
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` remains a hard filter (security boundary). `sourceAgent` remains a
514
- * hard filter (kept for completeness; no production caller currently passes
515
- * it). When neither project nor missionDomains is sent, scoring is byte-
516
- * identical to pre-#203 the kill-switch / no-op guarantee.
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; privacy/sourceAgent still do (they remain hard filters).
538
- const filtered = Boolean(privacy || sourceAgent);
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
- // privacy/sourceAgent are pushed into the FTS SQL (hard filters). project
544
- // is NOT (it is a soft affinity boost in computeScore as of #203).
545
- ftsCandidates = storage.searchFts(db, query, fetchLimit, privacy, sourceAgent);
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). privacy stays a
554
- // hard filter (security boundary); project was removed here (#203 — it is now
555
- // scored, not filtered). sourceAgent stays (see options doc).
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 (security) and sourceAgent stay as hard filters.
597
- if (privacy && !privacy.includes(mem.privacy))
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 and privacy.
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
- if (privacy) {
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, overridden by any tagged
15
- * domain flagged `compartment: true` (deliberate compartmentalization
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. Any tagged compartment domain wins first one in array order if the
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
- * 3. A null weight loses to any numeric weight (treated as -Infinity).
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[], compartments: Set<string>): string;
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: compartment override first, else argmax weight, LLM
155
- * order (memory_tags insertion order = rowid, written most-relevant-first by
156
- * storage.setMemoryTags) breaking exact-weight ties.
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, overridden by any tagged
16
- * domain flagged `compartment: true` (deliberate compartmentalization
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. Any tagged compartment domain wins first one in array order if the
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
- * 3. A null weight loses to any numeric weight (treated as -Infinity).
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, compartments) {
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: compartment override first, else argmax weight, LLM
317
- * order (memory_tags insertion order = rowid, written most-relevant-first by
318
- * storage.setMemoryTags) breaking exact-weight ties.
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
- const compartments = compartmentSet(domains);
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, compartments);
335
+ const primary = derivePrimary(entry.tags);
345
336
  if (primary !== entry.domain) {
346
337
  update.run(primary, memoryId);
347
338
  updated++;
@@ -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
 
@@ -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
- * compartment override first, else argmax weight, else first tag (all-null
73
- * weights). The whole update is one transaction so domain, tag set, and
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` stays a hard filter (security
153
- * boundary, not a relevance signal). `sourceAgent` stays a hard filter (kept
154
- * for completeness; no production caller of retrieve() currently passes it).
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, privacy?: string[], sourceAgent?: string): Array<Memory & {
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, source_session, project,
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 ?? "WORK", opts.memoryType ?? "episode");
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
- * compartment override first, else argmax weight, else first tag (all-null
196
- * weights). The whole update is one transaction so domain, tag set, and
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, options.compartments ?? new Set());
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` stays a hard filter (security
356
- * boundary, not a relevance signal). `sourceAgent` stays a hard filter (kept
357
- * for completeness; no production caller of retrieve() currently passes it).
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, privacy, sourceAgent) {
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, source_session, project,
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 ?? "WORK", mem.memoryType ?? "episode");
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
  }