@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.
- package/README.md +11 -0
- package/dist/capture.d.ts +18 -1
- package/dist/capture.js +3 -2
- package/dist/classify-domains.d.ts +1 -1
- package/dist/classify-domains.js +5 -7
- package/dist/cli.js +11 -3
- package/dist/cluster.d.ts +5 -4
- package/dist/cluster.js +2 -3
- package/dist/consolidate.js +3 -5
- package/dist/db.js +23 -0
- package/dist/dedup.js +1 -1
- package/dist/distiller.js +2 -2
- package/dist/domain-classify.d.ts +1 -1
- package/dist/domain-classify.js +1 -5
- package/dist/eval/run-eval.js +0 -1
- package/dist/index.js +0 -1
- package/dist/init.d.ts +165 -0
- package/dist/init.js +283 -57
- package/dist/mcp-server.js +71 -25
- package/dist/nightly.js +121 -12
- package/dist/nofit.d.ts +1 -1
- package/dist/nofit.js +1 -2
- package/dist/pi-transcript-reader.d.ts +1 -1
- package/dist/pi-transcript-reader.js +1 -1
- package/dist/prompts.js +0 -7
- package/dist/recall-index.d.ts +15 -19
- package/dist/recall-index.js +13 -23
- package/dist/redact.d.ts +2 -2
- package/dist/redact.js +2 -2
- package/dist/retrieval.d.ts +7 -7
- package/dist/retrieval.js +20 -24
- package/dist/schema-prototypes.d.ts +8 -13
- package/dist/schema-prototypes.js +13 -22
- package/dist/seed-lesson.js +0 -1
- package/dist/storage.d.ts +9 -12
- package/dist/storage.js +19 -21
- package/dist/telemetry.d.ts +13 -2
- package/dist/telemetry.js +5 -1
- package/dist/types.d.ts +70 -24
- package/domains.example.json +2 -3
- package/hermes-plugin/hicortex/README.md +3 -1
- package/hermes-plugin/hicortex/config.py +34 -3
- package/hermes-plugin/hicortex/plugin.yaml +1 -1
- package/hermes-plugin/hicortex/provider.py +7 -1
- 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
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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
|
`;
|
package/dist/recall-index.d.ts
CHANGED
|
@@ -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
|
|
79
|
-
*
|
|
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
|
|
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
|
|
89
|
-
* retrieval (zero-boost neutral, never a filter / penalty)
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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.
|
|
111
|
-
*
|
|
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
|
package/dist/recall-index.js
CHANGED
|
@@ -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
|
|
134
|
-
*
|
|
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.
|
|
145
|
-
*
|
|
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)
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
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.,
|
|
9
|
-
*
|
|
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.,
|
|
10
|
-
*
|
|
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.
|
package/dist/retrieval.d.ts
CHANGED
|
@@ -178,10 +178,12 @@ export interface EmbedFn {
|
|
|
178
178
|
*
|
|
179
179
|
* #203 retrieval scoping: `project` and `missionDomains` are SOFT affinity
|
|
180
180
|
* terms in computeScore (zero-boost neutral, never a penalty), NOT filters.
|
|
181
|
-
* `privacy`
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
181
|
+
* `privacy` is NOT a filter (0.16.x: the column is fully vestigial — stored,
|
|
182
|
+
* never filtered; the distiller no longer sets it and retrieval ignores it
|
|
183
|
+
* entirely). `sourceAgent` remains a hard filter (kept for completeness; no
|
|
184
|
+
* production caller currently passes it). When neither project nor
|
|
185
|
+
* missionDomains is sent, scoring is byte-identical to pre-#203 — the
|
|
186
|
+
* kill-switch / no-op guarantee.
|
|
185
187
|
*/
|
|
186
188
|
export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query: string, options?: {
|
|
187
189
|
limit?: number;
|
|
@@ -189,7 +191,6 @@ export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query:
|
|
|
189
191
|
* Formerly a hard WHERE filter (#192); softening removes cross-scope
|
|
190
192
|
* starvation without excluding anything. */
|
|
191
193
|
project?: string | null;
|
|
192
|
-
privacy?: string[];
|
|
193
194
|
sourceAgent?: string;
|
|
194
195
|
/** #203: Hermes mission domains (declared in plugin config). Soft domain
|
|
195
196
|
* affinity in computeScore via max overlapping memory_tags.weight. */
|
|
@@ -206,11 +207,10 @@ export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query:
|
|
|
206
207
|
queryEmbedding?: Float32Array;
|
|
207
208
|
}): Promise<MemorySearchResult[]>;
|
|
208
209
|
/**
|
|
209
|
-
* Get recent context, optionally filtered by project
|
|
210
|
+
* Get recent context, optionally filtered by project.
|
|
210
211
|
*/
|
|
211
212
|
export declare function searchRecent(db: Database.Database, options?: {
|
|
212
213
|
project?: string | null;
|
|
213
214
|
limit?: number;
|
|
214
|
-
privacy?: string[];
|
|
215
215
|
}): MemorySearchResult[];
|
|
216
216
|
export {};
|
package/dist/retrieval.js
CHANGED
|
@@ -510,15 +510,16 @@ function reciprocalRankFusion(rankedLists, k = DEFAULT_RRF_K) {
|
|
|
510
510
|
*
|
|
511
511
|
* #203 retrieval scoping: `project` and `missionDomains` are SOFT affinity
|
|
512
512
|
* terms in computeScore (zero-boost neutral, never a penalty), NOT filters.
|
|
513
|
-
* `privacy`
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
513
|
+
* `privacy` is NOT a filter (0.16.x: the column is fully vestigial — stored,
|
|
514
|
+
* never filtered; the distiller no longer sets it and retrieval ignores it
|
|
515
|
+
* entirely). `sourceAgent` remains a hard filter (kept for completeness; no
|
|
516
|
+
* production caller currently passes it). When neither project nor
|
|
517
|
+
* missionDomains is sent, scoring is byte-identical to pre-#203 — the
|
|
518
|
+
* kill-switch / no-op guarantee.
|
|
517
519
|
*/
|
|
518
520
|
async function retrieve(db, embedFn, query, options) {
|
|
519
521
|
const limit = options?.limit ?? recallDefaults.searchLimit;
|
|
520
522
|
const project = options?.project;
|
|
521
|
-
const privacy = options?.privacy;
|
|
522
523
|
const sourceAgent = options?.sourceAgent;
|
|
523
524
|
const missionDomains = options?.missionDomains;
|
|
524
525
|
const now = new Date();
|
|
@@ -534,15 +535,17 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
534
535
|
// over-fetch — the old flat limit*3 intersected a global top-15 with (for the
|
|
535
536
|
// median project) ~1% of the corpus, starving every filtered query.
|
|
536
537
|
// #203: project is NO LONGER a filter (soft affinity now), so it does not
|
|
537
|
-
// trigger over-fetch;
|
|
538
|
-
|
|
538
|
+
// trigger over-fetch; only sourceAgent still does (it remains a hard filter).
|
|
539
|
+
// 0.16.x: privacy is no longer a filter either (column is vestigial).
|
|
540
|
+
const filtered = Boolean(sourceAgent);
|
|
539
541
|
const fetchLimit = filtered ? Math.min(limit * 20, 200) : limit * 3;
|
|
540
542
|
let vecCandidates = storage.vectorSearch(db, queryEmbedding, fetchLimit, []);
|
|
541
543
|
let ftsCandidates = [];
|
|
542
544
|
try {
|
|
543
|
-
//
|
|
544
|
-
// is
|
|
545
|
-
|
|
545
|
+
// sourceAgent is pushed into the FTS SQL (hard filter). project is NOT (it
|
|
546
|
+
// is a soft affinity boost in computeScore as of #203). privacy is NOT
|
|
547
|
+
// (0.16.x: vestigial column, never filtered).
|
|
548
|
+
ftsCandidates = storage.searchFts(db, query, fetchLimit, sourceAgent);
|
|
546
549
|
}
|
|
547
550
|
catch {
|
|
548
551
|
// FTS5 search can fail on special characters; fall back to vector-only
|
|
@@ -550,12 +553,9 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
550
553
|
if (vecCandidates.length === 0 && ftsCandidates.length === 0) {
|
|
551
554
|
return [];
|
|
552
555
|
}
|
|
553
|
-
// Post-filter vector candidates (sqlite-vec can't filter).
|
|
554
|
-
// hard filter (
|
|
555
|
-
//
|
|
556
|
-
if (privacy) {
|
|
557
|
-
vecCandidates = vecCandidates.filter((c) => privacy.includes(c.privacy));
|
|
558
|
-
}
|
|
556
|
+
// Post-filter vector candidates (sqlite-vec can't filter). sourceAgent stays
|
|
557
|
+
// a hard filter (see options doc); project is scored not filtered (#203);
|
|
558
|
+
// privacy is no longer filtered (0.16.x — vestigial).
|
|
559
559
|
if (sourceAgent) {
|
|
560
560
|
vecCandidates = vecCandidates.filter((c) => c.source_agent === sourceAgent);
|
|
561
561
|
}
|
|
@@ -593,9 +593,8 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
593
593
|
if (!mem)
|
|
594
594
|
continue;
|
|
595
595
|
// #203: project check removed — project is a soft affinity in computeScore,
|
|
596
|
-
// not a filter. privacy
|
|
597
|
-
|
|
598
|
-
continue;
|
|
596
|
+
// not a filter. 0.16.x: privacy check removed — the column is vestigial,
|
|
597
|
+
// never filtered. sourceAgent stays a hard filter.
|
|
599
598
|
if (sourceAgent && mem.source_agent !== sourceAgent)
|
|
600
599
|
continue;
|
|
601
600
|
candidateMap.set(gid, { mem, distance: DEFAULT_GRAPH_DISTANCE, source: "graph" });
|
|
@@ -677,12 +676,11 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
677
676
|
return results;
|
|
678
677
|
}
|
|
679
678
|
/**
|
|
680
|
-
* Get recent context, optionally filtered by project
|
|
679
|
+
* Get recent context, optionally filtered by project.
|
|
681
680
|
*/
|
|
682
681
|
function searchRecent(db, options) {
|
|
683
682
|
const limit = options?.limit ?? recallDefaults.recentLimit;
|
|
684
683
|
const project = options?.project;
|
|
685
|
-
const privacy = options?.privacy;
|
|
686
684
|
const now = new Date();
|
|
687
685
|
// #192 breadth: 30 → 180-day default window (config recentWindowDays).
|
|
688
686
|
// "Recent" for a long-lived corpus is a season, not a month; the narrow
|
|
@@ -691,9 +689,7 @@ function searchRecent(db, options) {
|
|
|
691
689
|
if (project) {
|
|
692
690
|
candidates = candidates.filter((c) => c.project === project);
|
|
693
691
|
}
|
|
694
|
-
|
|
695
|
-
candidates = candidates.filter((c) => privacy.includes(c.privacy));
|
|
696
|
-
}
|
|
692
|
+
// 0.16.x: privacy filter removed — the column is vestigial, never filtered.
|
|
697
693
|
if (candidates.length === 0)
|
|
698
694
|
return [];
|
|
699
695
|
const allIds = candidates.map((c) => c.id);
|
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
* embedding of the domain's config description instead.
|
|
12
12
|
* - weight(memory, tag) = cosine(memory embedding, prototype(tag)). Both
|
|
13
13
|
* vectors are L2-normalized, so cosine reduces to a dot product.
|
|
14
|
-
* - PRIMARY (memories.domain) = argmax-weight tag,
|
|
15
|
-
*
|
|
16
|
-
* the owner's Work firewall). Fully mechanical, no LLM.
|
|
14
|
+
* - PRIMARY (memories.domain) = argmax-weight tag, with LLM tag order
|
|
15
|
+
* breaking exact-weight ties. Fully mechanical, no LLM.
|
|
17
16
|
*
|
|
18
17
|
* The LLM decides ONLY the discrete part (which schemas apply — see
|
|
19
18
|
* domain-classify.ts); ALL gradation is derived from embeddings here.
|
|
@@ -65,24 +64,20 @@ export interface WeightedTag {
|
|
|
65
64
|
tag: string;
|
|
66
65
|
weight: number | null;
|
|
67
66
|
}
|
|
68
|
-
/** The configured compartment domain names (DomainDef.compartment === true). */
|
|
69
|
-
export declare function compartmentSet(domains: DomainDef[]): Set<string>;
|
|
70
67
|
/**
|
|
71
68
|
* Derive the PRIMARY tag (memories.domain) from a weighted tag set.
|
|
72
69
|
*
|
|
73
70
|
* Rules (deterministic, no LLM):
|
|
74
|
-
* 1.
|
|
75
|
-
* (unusual) case of several arises.
|
|
76
|
-
* 2. Else the argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
71
|
+
* 1. The argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
77
72
|
* order: ties (and all-null weights) resolve to the EARLIEST array
|
|
78
73
|
* position — strict `>` comparison keeps the first maximum.
|
|
79
|
-
*
|
|
74
|
+
* 2. A null weight loses to any numeric weight (treated as -Infinity).
|
|
80
75
|
*
|
|
81
76
|
* Throws on an empty tag set — callers guarantee >= 1 tag (an empty tag set
|
|
82
77
|
* from the classifier is a NO-FIT and must be routed through nofit.ts, never
|
|
83
78
|
* here); an empty set reaching this function is a programming error.
|
|
84
79
|
*/
|
|
85
|
-
export declare function derivePrimary(tags: WeightedTag[]
|
|
80
|
+
export declare function derivePrimary(tags: WeightedTag[]): string;
|
|
86
81
|
export interface PrototypeStat {
|
|
87
82
|
domain: string;
|
|
88
83
|
memberCount: number;
|
|
@@ -151,9 +146,9 @@ export declare function recomputeAllTagWeights(db: Database.Database, prototypes
|
|
|
151
146
|
};
|
|
152
147
|
/**
|
|
153
148
|
* Re-derive the PRIMARY (memories.domain) of every tagged memory from its
|
|
154
|
-
* current tag weights:
|
|
155
|
-
*
|
|
156
|
-
*
|
|
149
|
+
* current tag weights: argmax weight, LLM order (memory_tags insertion order =
|
|
150
|
+
* rowid, written most-relevant-first by storage.setMemoryTags) breaking
|
|
151
|
+
* exact-weight ties.
|
|
157
152
|
*
|
|
158
153
|
* Memories with NO memory_tags rows are untouched (e.g. infra-skipped rows
|
|
159
154
|
* awaiting classification — issue #150 discipline).
|