@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/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
  }
package/dist/types.d.ts CHANGED
@@ -13,9 +13,23 @@ export interface Memory {
13
13
  ingested_at: string;
14
14
  source_agent: string;
15
15
  source_session: string | null;
16
+ /**
17
+ * Stable attribution id of the capturing client (a per-install UUID from
18
+ * config.json `agentId`). Survives agent/machine renames — unlike
19
+ * `source_agent` (a readable name). Attribution only; nothing filters on it.
20
+ * NULL on memories captured before this column existed. (0.16.x)
21
+ */
22
+ source_agent_id: string | null;
16
23
  project: string | null;
17
24
  domain: string | null;
18
- privacy: "PUBLIC" | "WORK" | "PERSONAL" | "SENSITIVE";
25
+ /**
26
+ * Provenance only (0.16.x): the client-declared topic/domain of the
27
+ * capturing agent (config.json `sourceDomain`). NOT used for recall filtering
28
+ * or scoring, and NOT the content-classified primary (that is `domain`
29
+ * above). NULL when the client declares none. Echoed back on /memory GET.
30
+ */
31
+ source_domain: string | null;
32
+ privacy: ("PUBLIC" | "WORK" | "PERSONAL" | "SENSITIVE") | null;
19
33
  memory_type: "episode" | "lesson" | "fact" | "decision";
20
34
  updated_at: string | null;
21
35
  }
@@ -157,6 +171,15 @@ export interface HicortexConfig {
157
171
  serverUrl?: string;
158
172
  /** Bearer token for the Hicortex server. Localhost bypasses auth by default. */
159
173
  authToken?: string;
174
+ /**
175
+ * Stable per-install UUID generated by `init` (see ensureAgentId in init.ts;
176
+ * never rotated). Attribution identity of the capturing client — stored on
177
+ * each captured memory as `source_agent_id` (see Memory.source_agent_id) and
178
+ * sent on every /distill segment. Survives agent/machine renames, unlike the
179
+ * readable `source_agent` name. Pure attribution; nothing filters or scopes
180
+ * on it. (0.16.x)
181
+ */
182
+ agentId?: string;
160
183
  /** @deprecated Use the Hicortex server for distillation and consolidation. */
161
184
  llmBaseUrl?: string;
162
185
  /** @deprecated Use the Hicortex server for distillation and consolidation. */
@@ -194,6 +217,21 @@ export interface HicortexConfig {
194
217
  consolidateHour?: number;
195
218
  /** @deprecated The OC plugin no longer opens its own database. */
196
219
  dbPath?: string;
220
+ /**
221
+ * Client-declared topic/domain of THIS capturing agent (provenance only,
222
+ * 0.16.x). Sent on captured memories as `source_domain` (see
223
+ * Memory.source_domain) — NOT used for recall filtering or scoring, and NOT
224
+ * the content-classified primary (which is the server-derived `domain`
225
+ * column on each memory, classified against `domains` below).
226
+ *
227
+ * DISTINCT from `domains` (plural) directly below: `domains` is the
228
+ * config-owned VOCABULARY — the server's life-sphere list that memories are
229
+ * content-classified against; this singular `sourceDomain` is the client
230
+ * declaring "I am an agent that works on topic X", recorded as provenance on
231
+ * what it captures. Do not conflate the two. (Renamed from `domain` in
232
+ * 0.16.x — one char from `domains`, meant something unrelated.)
233
+ */
234
+ sourceDomain?: string;
197
235
  /**
198
236
  * Optional config-owned domain list — the user's top-level memory spheres
199
237
  * (life areas OR project/topic areas). When present, the nightly multi-tag
@@ -205,7 +243,7 @@ export interface HicortexConfig {
205
243
  * Server-mode `init` scaffolds a generic 5-domain default (Work, Personal,
206
244
  * People, Health, Finance — see GENERIC_DEFAULT_DOMAINS in init.ts) when
207
245
  * this key is absent, and NEVER touches an existing list. A power-user
208
- * example (compartment flag, custom weakPrimaryFloor) ships as
246
+ * example (custom weakPrimaryFloor) ships as
209
247
  * domains.example.json in the package root.
210
248
  *
211
249
  * NO fallback bucket is needed or special-cased (owner amendment 07.07):
@@ -227,13 +265,6 @@ export interface HicortexConfig {
227
265
  export interface DomainDef {
228
266
  name: string;
229
267
  description: string;
230
- /**
231
- * Deliberate compartmentalization (graded-schema spec, 07.07.2026): when
232
- * true, this domain becomes the PRIMARY (memories.domain) whenever it is
233
- * tagged, overriding the argmax-weight rule. The owner's config flags only
234
- * Work — a work/life firewall. Optional; absent = false.
235
- */
236
- compartment?: boolean;
237
268
  }
238
269
  /** Response from license validation API. */
239
270
  export interface LicenseInfo {
@@ -286,8 +317,14 @@ export interface ModuleIndex {
286
317
  export interface InsertMemoryOptions {
287
318
  sourceAgent?: string;
288
319
  sourceSession?: string | null;
320
+ /** Stable client UUID (config.json `agentId`). Attribution only. */
321
+ sourceAgentId?: string | null;
322
+ /** Client-declared topic/domain of the capturing agent. Provenance only. */
323
+ sourceDomain?: string | null;
289
324
  project?: string | null;
290
- privacy?: string;
325
+ /** 0.16.x: vestigial — stored but never filtered. null (or absent) when the
326
+ * caller doesn't declare one; an explicit value is honored as-is. */
327
+ privacy?: string | null;
291
328
  memoryType?: string;
292
329
  baseStrength?: number;
293
330
  createdAt?: string;
@@ -297,16 +334,3 @@ export interface VectorSearchOptions {
297
334
  limit?: number;
298
335
  excludeIds?: string[];
299
336
  }
300
- /** Options for FTS search. */
301
- export interface FtsSearchOptions {
302
- limit?: number;
303
- privacy?: string[];
304
- sourceAgent?: string;
305
- }
306
- /** Options for retrieval. */
307
- export interface RetrievalOptions {
308
- limit?: number;
309
- project?: string | null;
310
- privacy?: string[];
311
- sourceAgent?: string;
312
- }
@@ -33,14 +33,13 @@
33
33
  "_powerUserExample": {
34
34
  "_readme": [
35
35
  "A narrower life-sphere set for users who want tighter buckets.",
36
- "`compartment: true` makes a domain the PRIMARY whenever it is taggeda deliberate work/life firewall.",
36
+ "The PRIMARY domain is derived by argmax association weight (LLM tag order breaks ties) no manual override flag.",
37
37
  "`weakPrimaryFloor` (default 0.45) is the minimum embedding similarity for a no-fit memory to earn a weak primary; tune it from your corpus."
38
38
  ],
39
39
  "domains": [
40
40
  {
41
41
  "name": "Work",
42
- "description": "Employer, day job, client projects, workstreams",
43
- "compartment": true
42
+ "description": "Employer, day job, client projects, workstreams"
44
43
  },
45
44
  {
46
45
  "name": "Personal",
@@ -22,7 +22,7 @@ That's the whole surface. No `sync_turn`, no compaction/session-end capture —
22
22
 
23
23
  ### Pushed recall index (0.7.0, server ≥ 0.14)
24
24
 
25
- Instead of injecting full memory content every turn, `prefetch` sends the user's message to the server's `POST /recall-index` and injects the returned **index block** verbatim — one line per memory (id, title, date), capped and relevance-gated server-side. The agent fetches full content with `hicortex_get(id)` only when a line is actually relevant; that fetch is what strengthens the memory (exposure ≠ use). All tuning knobs (`recallMaxItems`, `recallMinSimilarity`, `recallReshowTurns`, `recallMinPromptChars`, …) live in the **server** config — the plugin carries none. Dedup is turn-based and server-side per session; the plugin resets it at `initialize` (the Hermes `MemoryProvider` interface exposes no compaction signal, so a mid-session context rebuild cannot trigger a reset — the server's turn-based re-show window covers that gap). Against a pre-0.14 server (404) the plugin falls back to the 0.6.x `GET /search` full-content prefetch, fail-soft, re-probing the endpoint every 10 minutes so a later server upgrade is picked up without a gateway restart. The recall calls carry the profile's configured `privacy_filter`/`default_project` and use a short dedicated timeout (1.5 s) so a slow server can never stall a turn.
25
+ Instead of injecting full memory content every turn, `prefetch` sends the user's message to the server's `POST /recall-index` and injects the returned **index block** verbatim — one line per memory (id, title, date), capped and relevance-gated server-side. The agent fetches full content with `hicortex_get(id)` only when a line is actually relevant; that fetch is what strengthens the memory (exposure ≠ use). All tuning knobs (`recallMaxItems`, `recallMinSimilarity`, `recallReshowTurns`, `recallMinPromptChars`, …) live in the **server** config — the plugin carries none. Dedup is turn-based and server-side per session; the plugin resets it at `initialize` (the Hermes `MemoryProvider` interface exposes no compaction signal, so a mid-session context rebuild cannot trigger a reset — the server's turn-based re-show window covers that gap). Against a pre-0.14 server (404) the plugin falls back to the 0.6.x `GET /search` full-content prefetch, fail-soft, re-probing the endpoint every 10 minutes so a later server upgrade is picked up without a gateway restart. The recall calls carry the profile's configured `default_project` (and `mission_domains`) and use a short dedicated timeout (1.5 s) so a slow server can never stall a turn. (`privacy_filter` is deprecated since 0.7.2 — the server ignores privacy; see [Configuration](#configuration).)
26
26
 
27
27
  ### Per-agent standing context (0.13)
28
28
 
@@ -84,6 +84,8 @@ export HICORTEX_AUTH_TOKEN=hctx-default-token # or your custom token
84
84
 
85
85
  Env overrides: `HICORTEX_URL`, `HICORTEX_AUTH_TOKEN`.
86
86
 
87
+ > **`privacy_filter` is DEPRECATED** (plugin 0.7.2 / server 0.16.2). The server no longer filters on privacy — the `privacy` column is vestigial (stored, never filtered). The setting is still accepted for backward compatibility but is now a harmless no-op; setting it emits a one-time-per-process warning in the gateway log. For work/personal isolation, run a **separate Hicortex server** per scope rather than relying on in-server privacy filtering.
88
+
87
89
  ## Topology
88
90
 
89
91
  - **Server host:** runs Hicortex. Set `hicortex_url: http://localhost:8787` (localhost bypasses auth).
@@ -8,9 +8,15 @@ plugin also works with env-only setup.
8
8
  from __future__ import annotations
9
9
 
10
10
  import json
11
+ import logging
11
12
  import os
12
13
  from typing import Any, Dict, Optional
13
14
 
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # One-time-per-process guard for the privacy_filter deprecation warning.
18
+ _privacy_filter_deprecation_warned = False
19
+
14
20
  # Declarative config schema — drives `hermes memory setup` (see MemoryProvider
15
21
  # .get_config_schema). Field shape per the Hermes MemoryProvider contract:
16
22
  # key, label, description, default, required, secret, env_var, choices, url.
@@ -56,8 +62,15 @@ CONFIG_SCHEMA: list[dict[str, Any]] = [
56
62
  },
57
63
  {
58
64
  "key": "privacy_filter",
59
- "label": "Privacy filter",
60
- "description": "Comma-separated privacy levels to include (e.g. WORK,PERSONAL).",
65
+ "label": "Privacy filter (DEPRECATED)",
66
+ "description": (
67
+ "DEPRECATED since plugin 0.7.2 / server 0.16.2. The server no "
68
+ "longer filters on privacy — the column is vestigial. This setting "
69
+ "is now a harmless no-op: it is still accepted for backward "
70
+ "compat but ignored. For work/personal isolation, run a separate "
71
+ "Hicortex server per scope. (Historically: comma-separated privacy "
72
+ "levels to include, e.g. WORK,PERSONAL.)"
73
+ ),
61
74
  "default": "WORK,PERSONAL",
62
75
  "required": False,
63
76
  },
@@ -94,14 +107,19 @@ def _config_path(hermes_home: Optional[str] = None) -> str:
94
107
 
95
108
  def load_config() -> Dict[str, Any]:
96
109
  """Load merged config: file <- env overrides <- defaults."""
110
+ global _privacy_filter_deprecation_warned
97
111
  path = _config_path()
98
112
  cfg: Dict[str, Any] = {}
113
+ file_set_privacy_filter = False
99
114
  if os.path.exists(path):
100
115
  try:
101
116
  with open(path, encoding="utf-8") as f:
102
117
  cfg = json.load(f) or {}
103
118
  except Exception:
104
119
  cfg = {}
120
+ # Detect an EXPLICIT user setting (the default is applied via setdefault
121
+ # below); only warn when the profile actually configured it.
122
+ file_set_privacy_filter = "privacy_filter" in cfg
105
123
 
106
124
  # Env overrides
107
125
  if os.environ.get("HICORTEX_URL"):
@@ -113,6 +131,19 @@ def load_config() -> Dict[str, Any]:
113
131
  cfg.setdefault("hicortex_url", "http://localhost:8787")
114
132
  cfg.setdefault("recall_limit", 5)
115
133
  cfg.setdefault("privacy_filter", "WORK,PERSONAL")
134
+
135
+ # 0.16.2 deprecation: privacy_filter is a no-op now (server ignores privacy
136
+ # entirely). Warn once per process if the profile explicitly sets it.
137
+ if file_set_privacy_filter and not _privacy_filter_deprecation_warned:
138
+ _privacy_filter_deprecation_warned = True
139
+ logger.warning(
140
+ "hicortex: config.json sets 'privacy_filter', which is deprecated "
141
+ "since plugin 0.7.2 / server 0.16.2 — the server no longer filters "
142
+ "on privacy (the column is vestigial). It is a harmless no-op now. "
143
+ "For work/personal isolation, run a separate Hicortex server per "
144
+ "scope. (This warning fires once per process.)"
145
+ )
146
+
116
147
  return cfg
117
148
 
118
149
 
@@ -1,5 +1,5 @@
1
1
  name: hicortex
2
- version: 0.7.0
2
+ version: 0.7.2
3
3
  description: "Self-learning memory for Hermes agents — every session is distilled into lessons overnight, and your agent wakes up wiser. Pushes a compact per-turn recall index (lazy-loaded with hicortex_get), injects fresh lessons plus a per-agent standing context block, and exposes the full 9-tool memory surface (search, get, recent, ingest, lessons, index, graph, update, delete) via a shared Hicortex server. Stdlib-only."
4
4
  pip_dependencies: []
5
5
  hooks: []
@@ -446,6 +446,11 @@ class HicortexProvider(MemoryProvider):
446
446
  lines.append("Lessons:")
447
447
  for l in lessons:
448
448
  c = (l.get("content") or "").strip().replace("\n", " ")
449
+ # Legacy lessons were stored with a "## Lesson:" prefix; new ones are
450
+ # topic-first (selected by memory_type, not the prefix). Strip it so
451
+ # Hermes renders the same topic-first line as the CC/OC lessons blocks.
452
+ if c.startswith("## Lesson: "):
453
+ c = c[len("## Lesson: "):]
449
454
  lines.append(f"- {c[:200]}")
450
455
  if idx.get("total"):
451
456
  lines.append(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "Self-learning memory for AI agents — experience captured automatically, distilled into lessons overnight, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {