@gamaze/hicortex 0.16.1 → 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/dist/nightly.js CHANGED
@@ -71,15 +71,28 @@ const state_js_1 = require("./state.js");
71
71
  const capture_cursors_js_1 = require("./capture-cursors.js");
72
72
  const capture_js_1 = require("./capture.js");
73
73
  const telemetry_js_1 = require("./telemetry.js");
74
+ const init_js_1 = require("./init.js");
74
75
  const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
75
76
  function readNightlyConfig(stateDir) {
77
+ const configPath = (0, node_path_1.join)(stateDir, "config.json");
78
+ let loaded;
76
79
  try {
77
- const configPath = (0, node_path_1.join)(stateDir, "config.json");
78
- return JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
80
+ loaded = (0, init_js_1.loadConfigStrict)(configPath);
79
81
  }
80
- catch {
82
+ catch (e) {
83
+ // Malformed existing config (bad JSON / non-object / unreadable): visible
84
+ // WARN so the operator fixes it, then fail-soft to null. The strict load
85
+ // also protects the agentId self-heal below — its throw is now reachable
86
+ // here (without this routing, a swallowed parse → null → the `if
87
+ // (savedConfig)` guard would skip the self-heal entirely).
88
+ console.warn(`[hicortex] ${configPath} exists but could not be parsed — running degraded ` +
89
+ `(agentId self-heal and config-driven knobs will not apply this run). ` +
90
+ `Fix the JSON and re-run. Cause: ${e instanceof Error ? e.message : String(e)}`);
81
91
  return null;
82
92
  }
93
+ // ENOENT → hadFile=false → null (install not set up yet; silent, matches the
94
+ // old catch→null behavior).
95
+ return loaded.hadFile ? loaded.config : null;
83
96
  }
84
97
  function readConfigLicenseKey(stateDir) {
85
98
  try {
@@ -202,6 +215,17 @@ async function runNightly(options = {}) {
202
215
  (0, state_js_1.migrateLegacyState)(stateDir);
203
216
  // Check mode: client or server
204
217
  const savedConfig = readNightlyConfig(stateDir);
218
+ // 0.16.2 activation gap: pre-0.16.2 installs never re-run init, so their
219
+ // config has no agentId → capture sent source_agent_id: null forever (the
220
+ // provenance feature was inert for the whole existing fleet). Self-heal on
221
+ // the first nightly after upgrade: ensureAndPersistAgentId generates + writes
222
+ // the id once (idempotent thereafter). Mutate the in-memory savedConfig so
223
+ // BOTH capture paths (server line below, client via runClientNightly's param)
224
+ // read the value without re-reading the file.
225
+ if (savedConfig) {
226
+ const { agentId } = (0, init_js_1.ensureAndPersistAgentId)((0, node_path_1.join)(stateDir, "config.json"));
227
+ savedConfig.agentId = agentId;
228
+ }
205
229
  if (savedConfig?.mode === "client") {
206
230
  // --capture-only is accepted in client mode but irrelevant: client nightly
207
231
  // is already capture-only (no consolidation step).
@@ -308,10 +332,14 @@ async function runNightly(options = {}) {
308
332
  }
309
333
  // Step 2: pack each session's delta into ≤60K segments and POST to the
310
334
  // local daemon via /distill; cursors advance on confirmed success.
335
+ // source_agent_id / source_domain are per-client provenance from
336
+ // config.json (agentId / sourceDomain) — attribution only, no filtering.
311
337
  const result = await (0, capture_js_1.captureBatches)(batches, {
312
338
  post: makeLocalPost(port),
313
339
  cursorStore,
314
340
  dryRun,
341
+ sourceAgentId: savedConfig?.agentId,
342
+ sourceDomain: savedConfig?.sourceDomain,
315
343
  });
316
344
  memoriesIngested = result.memoriesIngested;
317
345
  // A 429/401 stop must hold the watermark too (fix 1): the loop abandoned
@@ -529,6 +557,10 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
529
557
  post: makeRemotePost(serverUrl, authToken),
530
558
  cursorStore,
531
559
  dryRun,
560
+ // Per-client provenance from config.json (agentId / sourceDomain). The
561
+ // server stores these alongside source_agent; nothing filters on them.
562
+ sourceAgentId: config.agentId,
563
+ sourceDomain: config.sourceDomain,
532
564
  });
533
565
  memoriesIngested = result.memoriesIngested;
534
566
  sessionsSent = result.sessionsSent;
package/dist/nofit.d.ts CHANGED
@@ -90,7 +90,7 @@ export declare function resolveNoFit(db: Database.Database, memoryId: string, do
90
90
  * primary derives naturally inside setMemoryTags). Logged distinctly so
91
91
  * weak primaries are auditable apart from LLM-tagged rows.
92
92
  */
93
- export declare function applyWeakPrimary(db: Database.Database, memoryId: string, domain: string, weight: number, compartments: Set<string>): void;
93
+ export declare function applyWeakPrimary(db: Database.Database, memoryId: string, domain: string, weight: number): void;
94
94
  /**
95
95
  * Apply no-association decay to a no-fit-below-floor memory:
96
96
  * - clear any leftover memory_tags rows (e.g. a legacy "Unsorted" tag from
package/dist/nofit.js CHANGED
@@ -137,10 +137,9 @@ function resolveNoFit(db, memoryId, domains, prototypes, floor) {
137
137
  * primary derives naturally inside setMemoryTags). Logged distinctly so
138
138
  * weak primaries are auditable apart from LLM-tagged rows.
139
139
  */
140
- function applyWeakPrimary(db, memoryId, domain, weight, compartments) {
140
+ function applyWeakPrimary(db, memoryId, domain, weight) {
141
141
  storage.setMemoryTags(db, memoryId, [domain], {
142
142
  weights: { [domain]: weight },
143
- compartments,
144
143
  });
145
144
  console.log(`[hicortex] weak-primary ${domain} w=${weight.toFixed(2)} for ${memoryId}`);
146
145
  }
package/dist/prompts.js CHANGED
@@ -105,8 +105,6 @@ EXTRACT into this markdown format:
105
105
 
106
106
  # Session Memory: ${date} - ${projectName}
107
107
 
108
- ## Classification: [pick one: PUBLIC / WORK / PERSONAL / SENSITIVE]
109
-
110
108
  ### Decisions Made
111
109
  - [SUBJECT]: [decision] — [reasoning] (${date})
112
110
 
@@ -153,11 +151,6 @@ RULES:
153
151
  the correction matters deeply. Note the intensity AFTER the subject, never before it
154
152
  (e.g. "Pricing tiers: strongly rejected per-agent billing — …", not
155
153
  "[Strong Negative] User rejected per-agent billing"). The subject always comes first.
156
- - PRIVACY CLASSIFICATION (one of):
157
- - PUBLIC: general tech knowledge, open-source patterns, publicly available info
158
- - WORK: project-specific decisions, architecture choices, client/business context
159
- - PERSONAL: personal preferences, family, health, lifestyle, private life
160
- - SENSITIVE: API keys mentioned, credentials, financial account details, medical records
161
154
  - Omit any section that has zero items (don't include empty sections)
162
155
  - If nothing worth extracting, output ONLY: "NO_EXTRACT"
163
156
  `;
@@ -81,17 +81,18 @@ export declare function formatIndexLine(r: MemorySearchResult & {
81
81
  */
82
82
  export declare function passesRelevanceGate(r: MemorySearchResult, minSimilarity: number): boolean;
83
83
  /** Recall filters a client may push per request (#193 review F1): a scoped
84
- * plugin (Hermes privacy_filter / default_project) must be able to narrow
84
+ * plugin (Hermes default_project / mission_domains) must be able to narrow
85
85
  * recall exactly like the legacy /search prefetch did — dropping them
86
86
  * silently would leak out-of-scope memory titles into the injected index.
87
87
  *
88
- * #203: `project` and `mission_domains` are now SOFT affinity signals in
89
- * retrieval (zero-boost neutral, never a filter / penalty); `privacy` stays a
90
- * hard filter (security boundary). They ride the body retrieveFn
91
- * retrieve() computeScore path unchanged in shape. */
88
+ * #203: `project` and `mission_domains` are SOFT affinity signals in
89
+ * retrieval (zero-boost neutral, never a filter / penalty). 0.16.x: `privacy`
90
+ * is gone from this shape entirely the column is vestigial, never filtered,
91
+ * so a plugin's `privacy_filter` is a harmless no-op the server no longer
92
+ * threads through. The body field is still ACCEPTED (backward compat) but
93
+ * ignored. */
92
94
  export interface RecallFilters {
93
95
  project?: string;
94
- privacy?: string[];
95
96
  /** #203: Hermes mission domains (declared in plugin config). Soft domain
96
97
  * affinity in computeScore via max overlapping memory_tags.weight. */
97
98
  mission_domains?: string[];
@@ -107,14 +108,9 @@ export interface RecallIndexDeps {
107
108
  }
108
109
  /** Normalize a request-supplied string-list param: array of strings or a CSV
109
110
  * string → string[] | undefined. Anything else (or an empty result) means
110
- * "absent" — never a partial guess. Shared by `parsePrivacyParam` and
111
- * `mission_domains` (#203) so both accept `["A","B"]` and `"A, B"` alike. */
111
+ * "absent" — never a partial guess. Used by `mission_domains` (#203) so it
112
+ * accepts `["A","B"]` and `"A, B"` alike. */
112
113
  export declare function parseStringListParam(v: unknown): string[] | undefined;
113
- /** Normalize a request-supplied privacy filter: array of strings or a CSV
114
- * string → string[] | undefined. Anything else (or an empty result) means
115
- * "no filter" — never a partial guess. Delegates to parseStringListParam;
116
- * kept as a named export for tests and handleMemoryGet callers. */
117
- export declare function parsePrivacyParam(v: unknown): string[] | undefined;
118
114
  /**
119
115
  * Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
120
116
  * all behavior lives here so tests exercise it directly.
@@ -127,14 +123,14 @@ export declare function handleRecallIndex(deps: RecallIndexDeps, body: unknown):
127
123
  *
128
124
  * - Short/prefix ids resolve via storage.resolveMemoryId (F6) — the 8-char
129
125
  * citation ids agents are taught must work here like on /update, /delete.
130
- * - Optional `privacy` filter (array or CSV): when present and the memory's
131
- * privacy level is not in the allowed set, respond 404 with the SAME
132
- * not-found message — a scoped client must not learn the memory exists.
133
126
  * - A successful fetch is real use: access_count + 1 (strengthen).
127
+ *
128
+ * 0.16.x: the `privacy` filter gate was removed — the column is vestigial and
129
+ * never filtered. Callers may still send a `privacy` field (backward compat)
130
+ * but it is ignored.
134
131
  */
135
132
  export declare function handleMemoryGet(db: Database.Database, query: {
136
133
  id?: unknown;
137
- privacy?: unknown;
138
134
  }): RecallIndexResult;
139
135
  /**
140
136
  * MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
@@ -58,7 +58,6 @@ exports.memoryTitle = memoryTitle;
58
58
  exports.formatIndexLine = formatIndexLine;
59
59
  exports.passesRelevanceGate = passesRelevanceGate;
60
60
  exports.parseStringListParam = parseStringListParam;
61
- exports.parsePrivacyParam = parsePrivacyParam;
62
61
  exports.handleRecallIndex = handleRecallIndex;
63
62
  exports.handleMemoryGet = handleMemoryGet;
64
63
  exports.formatMemoryGetText = formatMemoryGetText;
@@ -141,8 +140,8 @@ function passesRelevanceGate(r, minSimilarity) {
141
140
  }
142
141
  /** Normalize a request-supplied string-list param: array of strings or a CSV
143
142
  * string → string[] | undefined. Anything else (or an empty result) means
144
- * "absent" — never a partial guess. Shared by `parsePrivacyParam` and
145
- * `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. */
146
145
  function parseStringListParam(v) {
147
146
  const items = Array.isArray(v)
148
147
  ? v.filter((x) => typeof x === "string")
@@ -152,13 +151,6 @@ function parseStringListParam(v) {
152
151
  const cleaned = items.map((s) => s.trim()).filter(Boolean);
153
152
  return cleaned.length > 0 ? cleaned : undefined;
154
153
  }
155
- /** Normalize a request-supplied privacy filter: array of strings or a CSV
156
- * string → string[] | undefined. Anything else (or an empty result) means
157
- * "no filter" — never a partial guess. Delegates to parseStringListParam;
158
- * kept as a named export for tests and handleMemoryGet callers. */
159
- function parsePrivacyParam(v) {
160
- return parseStringListParam(v);
161
- }
162
154
  /**
163
155
  * Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
164
156
  * all behavior lives here so tests exercise it directly.
@@ -185,13 +177,13 @@ async function handleRecallIndex(deps, body) {
185
177
  const minSimilarity = clampNumber(deps.options?.minSimilarity, DEFAULT_MIN_SIMILARITY, 0, 1);
186
178
  const turn = deps.registry.beginTurn(sessionId);
187
179
  // Optional client-side scoping (F1 + #203): project + mission_domains (soft
188
- // affinity) and privacy (hard filter) ride the body and are pushed into
189
- // retrieval. project is cwd-derived (CC/OC) or gateway-supplied; mission_domains
190
- // is Hermes-declared (plugin config). Neither excludes anything — both are
191
- // 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.)
192
185
  const filters = {
193
186
  project: typeof req.project === "string" && req.project ? req.project : undefined,
194
- privacy: parsePrivacyParam(req.privacy),
195
187
  mission_domains: parseStringListParam(req.mission_domains),
196
188
  };
197
189
  let results;
@@ -238,10 +230,11 @@ async function handleRecallIndex(deps, body) {
238
230
  *
239
231
  * - Short/prefix ids resolve via storage.resolveMemoryId (F6) — the 8-char
240
232
  * citation ids agents are taught must work here like on /update, /delete.
241
- * - Optional `privacy` filter (array or CSV): when present and the memory's
242
- * privacy level is not in the allowed set, respond 404 with the SAME
243
- * not-found message — a scoped client must not learn the memory exists.
244
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.
245
238
  */
246
239
  function handleMemoryGet(db, query) {
247
240
  const id = typeof query.id === "string" ? query.id : "";
@@ -257,9 +250,6 @@ function handleMemoryGet(db, query) {
257
250
  const mem = storage.getMemory(db, fullId);
258
251
  if (!mem)
259
252
  return notFound;
260
- const privacy = parsePrivacyParam(query.privacy);
261
- if (privacy && !privacy.includes(mem.privacy))
262
- return notFound;
263
253
  storage.strengthenMemory(db, fullId, new Date().toISOString());
264
254
  // `citation` is server-rendered so every plugin surfaces the same built-in
265
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++;
@@ -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;