@gamaze/hicortex 0.16.1 → 0.16.3

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 (45) hide show
  1. package/README.md +11 -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 +11 -3
  7. package/dist/cluster.d.ts +5 -4
  8. package/dist/cluster.js +2 -3
  9. package/dist/consolidate.js +3 -5
  10. package/dist/db.js +23 -0
  11. package/dist/dedup.js +1 -1
  12. package/dist/distiller.js +2 -2
  13. package/dist/domain-classify.d.ts +1 -1
  14. package/dist/domain-classify.js +1 -5
  15. package/dist/eval/run-eval.js +0 -1
  16. package/dist/index.js +0 -1
  17. package/dist/init.d.ts +165 -0
  18. package/dist/init.js +283 -57
  19. package/dist/mcp-server.js +71 -25
  20. package/dist/nightly.js +121 -12
  21. package/dist/nofit.d.ts +1 -1
  22. package/dist/nofit.js +1 -2
  23. package/dist/pi-transcript-reader.d.ts +1 -1
  24. package/dist/pi-transcript-reader.js +1 -1
  25. package/dist/prompts.js +0 -7
  26. package/dist/recall-index.d.ts +15 -19
  27. package/dist/recall-index.js +13 -23
  28. package/dist/redact.d.ts +2 -2
  29. package/dist/redact.js +2 -2
  30. package/dist/retrieval.d.ts +7 -7
  31. package/dist/retrieval.js +20 -24
  32. package/dist/schema-prototypes.d.ts +8 -13
  33. package/dist/schema-prototypes.js +13 -22
  34. package/dist/seed-lesson.js +0 -1
  35. package/dist/storage.d.ts +9 -12
  36. package/dist/storage.js +19 -21
  37. package/dist/telemetry.d.ts +13 -2
  38. package/dist/telemetry.js +5 -1
  39. package/dist/types.d.ts +70 -24
  40. package/domains.example.json +2 -3
  41. package/hermes-plugin/hicortex/README.md +3 -1
  42. package/hermes-plugin/hicortex/config.py +34 -3
  43. package/hermes-plugin/hicortex/plugin.yaml +1 -1
  44. package/hermes-plugin/hicortex/provider.py +7 -1
  45. package/package.json +1 -1
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 {
@@ -174,6 +187,27 @@ function captureLockWaitMs() {
174
187
  const env = Number(process.env.HICORTEX_CAPTURE_LOCK_WAIT_MS);
175
188
  return Number.isFinite(env) && env >= 0 ? env : CAPTURE_LOCK_WAIT_MS;
176
189
  }
190
+ /**
191
+ * Read a positive finite number from a nightly config key, falling back to
192
+ * `def` when the key is absent/invalid. Used by the pre-flight retry knobs
193
+ * (#163): tuning knobs live in config, never hardcoded (cf. decayHalfLifeDays,
194
+ * recallMinSimilarity).
195
+ *
196
+ * A value that is PRESENT but rejected (non-number, non-finite, or ≤ 0) warns
197
+ * — e.g. an operator who sets `preflightAttempts: 0` intending "don't retry"
198
+ * would otherwise silently get the default 3. (Single-try is
199
+ * `preflightAttempts: 1`, so there's no functional gap — this just makes the
200
+ * silent coercion visible, consistent with the fail-explicit pattern.)
201
+ */
202
+ function readPositiveConfig(config, key, def) {
203
+ const v = config[key];
204
+ if (v === undefined)
205
+ return def;
206
+ if (typeof v === "number" && Number.isFinite(v) && v > 0)
207
+ return v;
208
+ console.warn(`[hicortex] config "${key}" = ${String(v)} is not a positive finite number — using default ${def}.`);
209
+ return def;
210
+ }
177
211
  const NIGHTLY_LOG_MAX_BYTES = 1024 * 1024; // 1 MB — years of normal runs
178
212
  /**
179
213
  * Keep ~/.hicortex/nightly.log bounded. The launchd plist and systemd unit
@@ -202,6 +236,17 @@ async function runNightly(options = {}) {
202
236
  (0, state_js_1.migrateLegacyState)(stateDir);
203
237
  // Check mode: client or server
204
238
  const savedConfig = readNightlyConfig(stateDir);
239
+ // 0.16.2 activation gap: pre-0.16.2 installs never re-run init, so their
240
+ // config has no agentId → capture sent source_agent_id: null forever (the
241
+ // provenance feature was inert for the whole existing fleet). Self-heal on
242
+ // the first nightly after upgrade: ensureAndPersistAgentId generates + writes
243
+ // the id once (idempotent thereafter). Mutate the in-memory savedConfig so
244
+ // BOTH capture paths (server line below, client via runClientNightly's param)
245
+ // read the value without re-reading the file.
246
+ if (savedConfig) {
247
+ const { agentId } = (0, init_js_1.ensureAndPersistAgentId)((0, node_path_1.join)(stateDir, "config.json"));
248
+ savedConfig.agentId = agentId;
249
+ }
205
250
  if (savedConfig?.mode === "client") {
206
251
  // --capture-only is accepted in client mode but irrelevant: client nightly
207
252
  // is already capture-only (no consolidation step).
@@ -308,10 +353,14 @@ async function runNightly(options = {}) {
308
353
  }
309
354
  // Step 2: pack each session's delta into ≤60K segments and POST to the
310
355
  // local daemon via /distill; cursors advance on confirmed success.
356
+ // source_agent_id / source_domain are per-client provenance from
357
+ // config.json (agentId / sourceDomain) — attribution only, no filtering.
311
358
  const result = await (0, capture_js_1.captureBatches)(batches, {
312
359
  post: makeLocalPost(port),
313
360
  cursorStore,
314
361
  dryRun,
362
+ sourceAgentId: savedConfig?.agentId,
363
+ sourceDomain: savedConfig?.sourceDomain,
315
364
  });
316
365
  memoriesIngested = result.memoriesIngested;
317
366
  // A 429/401 stop must hold the watermark too (fix 1): the loop abandoned
@@ -461,16 +510,72 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
461
510
  const authToken = config.authToken;
462
511
  console.log(`[hicortex] Client nightly starting${dryRun ? " (dry run)" : ""}`);
463
512
  console.log(`[hicortex] Server: ${serverUrl}`);
464
- // Verify server is reachable
465
- try {
466
- const resp = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(5000) });
467
- if (!resp.ok)
468
- throw new Error(`HTTP ${resp.status}`);
469
- const data = await resp.json();
470
- console.log(`[hicortex] Server OK: v${data.version}, ${data.memories} memories`);
513
+ // Verify server is reachable. Retry so a client waking from sleep (its
514
+ // network link not yet re-established) or a transient blip doesn't abort
515
+ // the whole run the pre-flight only needs the link back, which can take
516
+ // ~1 min after wake.
517
+ //
518
+ // Config-overridable (#163): a wired Pi vs a sleeping laptop want different
519
+ // values. Defaults: 15s per-attempt timeout, 3 attempts, 60s gap.
520
+ //
521
+ // WALL-CLOCK NOTE: setTimeout and AbortSignal.timeout do NOT advance while
522
+ // macOS is asleep, so the ~2m45s worst case (3×15s + 2×60s) is wall-clock-
523
+ // optimistic — a sleeping laptop can straddle sleep cycles and the real
524
+ // elapsed time can exceed it. Not a defect: the capture lock isn't held
525
+ // during the retry and the cursor design is dup-over-loss, so a late success
526
+ // is harmless. Just don't treat 2m45s as a hard wall-clock bound.
527
+ const PREFLIGHT_TIMEOUT_MS = readPositiveConfig(config, "preflightTimeoutMs", 15_000);
528
+ const PREFLIGHT_ATTEMPTS = Math.max(1, Math.floor(readPositiveConfig(config, "preflightAttempts", 3)));
529
+ const PREFLIGHT_RETRY_GAP_MS = readPositiveConfig(config, "preflightRetryGapMs", 60_000);
530
+ let reachable = false;
531
+ for (let attempt = 1; attempt <= PREFLIGHT_ATTEMPTS; attempt++) {
532
+ try {
533
+ const resp = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS) });
534
+ if (!resp.ok)
535
+ throw new Error(`HTTP ${resp.status}`);
536
+ const data = await resp.json();
537
+ console.log(`[hicortex] Server OK: v${data.version}, ${data.memories} memories`);
538
+ reachable = true;
539
+ break;
540
+ }
541
+ catch (err) {
542
+ const msg = err instanceof Error ? err.message : String(err);
543
+ if (attempt < PREFLIGHT_ATTEMPTS) {
544
+ console.error(`[hicortex] Server unreachable at ${serverUrl} (attempt ${attempt}/${PREFLIGHT_ATTEMPTS}): ${msg} — retrying in ${PREFLIGHT_RETRY_GAP_MS / 1000}s`);
545
+ await new Promise((r) => setTimeout(r, PREFLIGHT_RETRY_GAP_MS));
546
+ }
547
+ else {
548
+ console.error(`[hicortex] Server unreachable at ${serverUrl} after ${PREFLIGHT_ATTEMPTS} attempts: ${msg}`);
549
+ }
550
+ }
471
551
  }
472
- catch (err) {
473
- console.error(`[hicortex] Server unreachable at ${serverUrl}: ${err instanceof Error ? err.message : String(err)}`);
552
+ if (!reachable) {
553
+ // The abort was invisible for weeks once: a plain `return` let the oneshot
554
+ // exit 0, so systemd/launchd recorded success and the capture gap went
555
+ // unnoticed. Exit non-zero so `systemctl --user status` / launchd show the
556
+ // unit failed (safe — this is a timer-driven oneshot with no Restart=, so
557
+ // no loop), and fire the telemetry ping (ok=false) so the abort is
558
+ // distinguishable from "powered off / uninstalled" in the activity aggregate.
559
+ process.exitCode = 1;
560
+ if (!dryRun && (0, telemetry_js_1.isTelemetryEnabled)(config)) {
561
+ await (0, telemetry_js_1.sendTelemetry)({
562
+ id: (0, telemetry_js_1.getTelemetryId)(stateDir),
563
+ v: VERSION,
564
+ pv: telemetry_js_1.TELEMETRY_PAYLOAD_VERSION,
565
+ event: "nightly",
566
+ mode: "client",
567
+ // `agent` deliberately OMITTED: no transcripts have been read at
568
+ // pre-flight, so the type is genuinely unknown. The admin summary
569
+ // buckets a missing agent as "?" (distinct from cc/pi/oc/mixed) —
570
+ // sending "cc" here would miscount an aborting Hermes/OC-only client
571
+ // as a cc install. The success-path ping sends the real type once
572
+ // session sources are known.
573
+ mem: 0,
574
+ lessons: 0,
575
+ sessions: 0,
576
+ ok: false,
577
+ });
578
+ }
474
579
  console.error(`[hicortex] Aborting. Will retry next run.`);
475
580
  return; // Don't update last-run so we retry
476
581
  }
@@ -529,6 +634,10 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
529
634
  post: makeRemotePost(serverUrl, authToken),
530
635
  cursorStore,
531
636
  dryRun,
637
+ // Per-client provenance from config.json (agentId / sourceDomain). The
638
+ // server stores these alongside source_agent; nothing filters on them.
639
+ sourceAgentId: config.agentId,
640
+ sourceDomain: config.sourceDomain,
532
641
  });
533
642
  memoriesIngested = result.memoriesIngested;
534
643
  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
  }
@@ -19,7 +19,7 @@
19
19
  * --home-alice-projects-myagent--/
20
20
  * 2026-04-10T18-37-44-615Z_<uuid>.jsonl
21
21
  * 2026-04-11T07-51-28-282Z_<uuid>.jsonl
22
- * --home-agents-Development-MAIC--/
22
+ * --home-user-projects-ExampleApp--/
23
23
  * ...
24
24
  *
25
25
  * The encoded-cwd uses double-dash separators: /home/alice/projects/myagent
@@ -20,7 +20,7 @@
20
20
  * --home-alice-projects-myagent--/
21
21
  * 2026-04-10T18-37-44-615Z_<uuid>.jsonl
22
22
  * 2026-04-11T07-51-28-282Z_<uuid>.jsonl
23
- * --home-agents-Development-MAIC--/
23
+ * --home-user-projects-ExampleApp--/
24
24
  * ...
25
25
  *
26
26
  * The encoded-cwd uses double-dash separators: /home/alice/projects/myagent
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
  `;
@@ -75,23 +75,24 @@ export declare function formatIndexLine(r: MemorySearchResult & {
75
75
  * NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
76
76
  * weight toward FTS-sourced entries. In practice FTS is currently inert on
77
77
  * real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
78
- * a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
79
- * is safe as measured. But FTS quality is unmeasured; if FTS starts firing
78
+ * a relevance sample returned 96/96 vector — so the floor change is safe as
79
+ * measured. But FTS quality is unmeasured; if FTS starts firing
80
80
  * (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
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;
@@ -130,8 +129,8 @@ function formatIndexLine(r, maxLen = DEFAULT_TITLE_CHARS) {
130
129
  * NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
131
130
  * weight toward FTS-sourced entries. In practice FTS is currently inert on
132
131
  * real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
133
- * a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
134
- * is safe as measured. But FTS quality is unmeasured; if FTS starts firing
132
+ * a relevance sample returned 96/96 vector — so the floor change is safe as
133
+ * measured. But FTS quality is unmeasured; if FTS starts firing
135
134
  * (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
136
135
  */
137
136
  function passesRelevanceGate(r, minSimilarity) {
@@ -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.
package/dist/redact.d.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * Why this exists:
6
6
  * - Session transcripts contain tool output: file reads, command output,
7
7
  * env var dumps. These regularly contain API keys, tokens, and paths.
8
- * - The distillation LLM is often remote (e.g., Ollama on MBP via
9
- * Tailscale). Secrets in the transcript travel over the network.
8
+ * - The distillation LLM is often remote (e.g., a mesh VPN link to a
9
+ * GPU box). Secrets in the transcript travel over the network.
10
10
  * - Even if the LLM correctly classifies the memory as SENSITIVE, the
11
11
  * secret is already stored and searchable via hicortex_search.
12
12
  * - Redaction runs BEFORE the LLM sees the text, eliminating the risk.
package/dist/redact.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * Why this exists:
7
7
  * - Session transcripts contain tool output: file reads, command output,
8
8
  * env var dumps. These regularly contain API keys, tokens, and paths.
9
- * - The distillation LLM is often remote (e.g., Ollama on MBP via
10
- * Tailscale). Secrets in the transcript travel over the network.
9
+ * - The distillation LLM is often remote (e.g., a mesh VPN link to a
10
+ * GPU box). Secrets in the transcript travel over the network.
11
11
  * - Even if the LLM correctly classifies the memory as SENSITIVE, the
12
12
  * secret is already stored and searchable via hicortex_search.
13
13
  * - Redaction runs BEFORE the LLM sees the text, eliminating the risk.
@@ -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).