@gamaze/hicortex 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -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 +10 -2
- package/dist/cluster.d.ts +5 -4
- package/dist/cluster.js +2 -3
- package/dist/consolidate.js +6 -5
- package/dist/db.js +23 -0
- package/dist/dedup.js +1 -1
- package/dist/distiller.js +19 -12
- package/dist/domain-classify.d.ts +1 -1
- package/dist/domain-classify.js +1 -5
- package/dist/eval/relevance-eval.d.ts +64 -0
- package/dist/eval/relevance-eval.js +1954 -0
- package/dist/eval/run-eval.js +0 -1
- package/dist/index.js +3 -3
- package/dist/init.d.ts +165 -0
- package/dist/init.js +283 -57
- package/dist/lessons-context.js +3 -2
- package/dist/mcp-server.js +72 -25
- package/dist/nightly.js +35 -3
- package/dist/nofit.d.ts +1 -1
- package/dist/nofit.js +1 -2
- package/dist/prompts.js +22 -13
- package/dist/recall-index.d.ts +56 -21
- package/dist/recall-index.js +51 -29
- 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.d.ts +1 -1
- package/dist/seed-lesson.js +1 -2
- package/dist/storage.d.ts +9 -12
- package/dist/storage.js +19 -21
- package/dist/types.d.ts +47 -23
- package/domains.example.json +2 -3
- package/hermes-plugin/hicortex/README.md +3 -1
- package/hermes-plugin/hicortex/config.py +33 -2
- package/hermes-plugin/hicortex/plugin.yaml +1 -1
- package/hermes-plugin/hicortex/provider.py +5 -0
- package/package.json +2 -1
package/dist/lessons-context.js
CHANGED
|
@@ -90,10 +90,11 @@ async function fetchLessonsBlock(cfg) {
|
|
|
90
90
|
const project = (0, node_path_1.basename)(process.cwd()) || null;
|
|
91
91
|
const selected = await (0, extensions_js_1.getLessonSelector)().select(data.lessons, { maxLessons, moduleIndex, project });
|
|
92
92
|
const lessonLines = selected.map((l) => {
|
|
93
|
-
const titleMatch = l.content.match(/## Lesson: (.+)/);
|
|
94
93
|
const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
|
|
95
94
|
const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
|
|
96
|
-
|
|
95
|
+
// First line, with any legacy `## Lesson:` prefix stripped — new lessons
|
|
96
|
+
// are stored topic-first without the prefix (memory_type carries the type).
|
|
97
|
+
const title = l.content.replace(/^##\s*Lesson:\s*/i, "").split("\n")[0].slice(0, 150);
|
|
97
98
|
const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
|
|
98
99
|
return `- ${title}${meta ? ` (${meta})` : ""}`;
|
|
99
100
|
});
|
package/dist/mcp-server.js
CHANGED
|
@@ -70,6 +70,7 @@ const recall_index_js_1 = require("./recall-index.js");
|
|
|
70
70
|
const seed_lesson_js_1 = require("./seed-lesson.js");
|
|
71
71
|
const distiller_js_1 = require("./distiller.js");
|
|
72
72
|
const dedup_js_1 = require("./dedup.js");
|
|
73
|
+
const init_js_1 = require("./init.js");
|
|
73
74
|
// ---------------------------------------------------------------------------
|
|
74
75
|
// Server state
|
|
75
76
|
// ---------------------------------------------------------------------------
|
|
@@ -78,6 +79,21 @@ let llm = null;
|
|
|
78
79
|
// llmConfig is module-level so the /distill handler can call resolveDistillFallback
|
|
79
80
|
// without having to read config on every request. null when no LLM is configured.
|
|
80
81
|
let llmConfig = null;
|
|
82
|
+
// One-time-per-process deprecation warning for the `?privacy=` query param
|
|
83
|
+
// (0.16.x: the column is vestigial, never filtered). Old clients/plugins still
|
|
84
|
+
// send it; we accept it (backward compat) but warn ONCE so an operator relying
|
|
85
|
+
// on privacy filtering discovers from the logs that it is now a no-op.
|
|
86
|
+
let privacyDeprecationWarned = false;
|
|
87
|
+
function warnDeprecatedPrivacyParamIfPresent(query, route) {
|
|
88
|
+
if (privacyDeprecationWarned)
|
|
89
|
+
return;
|
|
90
|
+
if (query.privacy === undefined || query.privacy === null || query.privacy === "")
|
|
91
|
+
return;
|
|
92
|
+
privacyDeprecationWarned = true;
|
|
93
|
+
console.warn(`[hicortex] client sent ?privacy= on /${route}, which is ignored since 0.16.2 — ` +
|
|
94
|
+
`privacy is no longer filtered server-side (the column is vestigial). ` +
|
|
95
|
+
`Use a separate Hicortex server for isolation. (This warning fires once per process.)`);
|
|
96
|
+
}
|
|
81
97
|
// distillFallbackMode controls whether a failed remote distill endpoint causes an
|
|
82
98
|
// immediate abort ("strict", default) or a fallback to the base model ("local").
|
|
83
99
|
let distillFallbackMode = "strict";
|
|
@@ -174,7 +190,6 @@ function createMcpServer() {
|
|
|
174
190
|
sourceAgent: "claude-code/manual",
|
|
175
191
|
project,
|
|
176
192
|
memoryType: memory_type ?? "episode",
|
|
177
|
-
privacy: "WORK",
|
|
178
193
|
});
|
|
179
194
|
return { content: [{ type: "text", text: `Memory stored (id: ${id.slice(0, 8)})` }] };
|
|
180
195
|
}
|
|
@@ -360,6 +375,17 @@ async function startServer(options = {}) {
|
|
|
360
375
|
// goes through resolveExplicitLlmConfig which requires a user-chosen provider.
|
|
361
376
|
// If nothing is configured: start recall-only with an unmissable warning.
|
|
362
377
|
const savedConfig = (0, llm_js_1.applyModelsBlock)(readConfigFile(stateDir));
|
|
378
|
+
// 0.16.2 activation gap: self-heal the agentId provenance field for
|
|
379
|
+
// pre-0.16.2 server installs on first boot after upgrade. The server's own
|
|
380
|
+
// nightly captures its sessions to localhost:8787/distill and needs this id;
|
|
381
|
+
// without it every self-captured memory landed with source_agent_id NULL.
|
|
382
|
+
// Hardened wrapper: throws on a malformed config instead of wiping, saves
|
|
383
|
+
// only when a new id was generated. Mutate savedConfig so any downstream
|
|
384
|
+
// read picks up the id even before the file is re-read.
|
|
385
|
+
if (savedConfig) {
|
|
386
|
+
const { agentId } = (0, init_js_1.ensureAndPersistAgentId)((0, node_path_1.join)(stateDir, "config.json"));
|
|
387
|
+
savedConfig.agentId = agentId;
|
|
388
|
+
}
|
|
363
389
|
if (savedConfig?.llmBackend === "claude-cli") {
|
|
364
390
|
const claudePath = (0, llm_js_1.findClaudeBinary)();
|
|
365
391
|
if (claudePath) {
|
|
@@ -494,6 +520,7 @@ async function startServer(options = {}) {
|
|
|
494
520
|
minSimilarity: savedConfig?.recallMinSimilarity,
|
|
495
521
|
maxItems: savedConfig?.recallMaxItems,
|
|
496
522
|
minPromptLength: savedConfig?.recallMinPromptChars,
|
|
523
|
+
titleChars: savedConfig?.recallTitleChars,
|
|
497
524
|
};
|
|
498
525
|
memoryInstructionsEnabled = savedConfig?.memoryInstructions !== false;
|
|
499
526
|
if (resolvedAgents.dropped.length > 0) {
|
|
@@ -596,7 +623,7 @@ async function startServer(options = {}) {
|
|
|
596
623
|
res.status(503).json({ error: "Server not initialized" });
|
|
597
624
|
return;
|
|
598
625
|
}
|
|
599
|
-
const { content, source_agent, project, memory_type, privacy, source_session, session_date } = req.body ?? {};
|
|
626
|
+
const { content, source_agent, source_agent_id, source_domain, project, memory_type, privacy, source_session, session_date } = req.body ?? {};
|
|
600
627
|
if (!content || typeof content !== "string") {
|
|
601
628
|
res.status(400).json({ error: "Missing or invalid 'content' field" });
|
|
602
629
|
return;
|
|
@@ -618,10 +645,15 @@ async function startServer(options = {}) {
|
|
|
618
645
|
const embedding = await (0, embedder_js_1.embed)(content);
|
|
619
646
|
const id = storage.insertMemory(db, content, embedding, {
|
|
620
647
|
sourceAgent: source_agent ?? "remote-client",
|
|
648
|
+
// Attribution + provenance passthrough (0.16.x); null when absent.
|
|
649
|
+
sourceAgentId: typeof source_agent_id === "string" ? source_agent_id : null,
|
|
650
|
+
sourceDomain: typeof source_domain === "string" ? source_domain : null,
|
|
621
651
|
sourceSession: source_session ?? undefined,
|
|
622
652
|
project: project ?? undefined,
|
|
623
653
|
memoryType: memory_type ?? "episode",
|
|
624
|
-
|
|
654
|
+
// 0.16.x: privacy defaults to null (vestigial column). A legacy client
|
|
655
|
+
// that sends an explicit value is honored; absent → null.
|
|
656
|
+
privacy: typeof privacy === "string" ? privacy : null,
|
|
625
657
|
createdAt: session_date ? new Date(session_date).toISOString() : undefined,
|
|
626
658
|
});
|
|
627
659
|
res.status(201).json({ id, message: "Memory ingested" });
|
|
@@ -645,11 +677,12 @@ async function startServer(options = {}) {
|
|
|
645
677
|
// No hardcoded default: absent limit → config-driven (searchLimit).
|
|
646
678
|
const limit = req.query.limit ? Number(req.query.limit) : undefined;
|
|
647
679
|
const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
680
|
+
// 0.16.x: `privacy` query param is ACCEPTED for backward compat (old
|
|
681
|
+
// clients/plugins still send it) but no longer read — retrieval ignores
|
|
682
|
+
// privacy entirely (the column is vestigial, never filtered).
|
|
683
|
+
warnDeprecatedPrivacyParamIfPresent(req.query, "search");
|
|
651
684
|
try {
|
|
652
|
-
const results = await retrieval.retrieve(db, embedder_js_1.embed, query, { limit, project
|
|
685
|
+
const results = await retrieval.retrieve(db, embedder_js_1.embed, query, { limit, project });
|
|
653
686
|
res.json({ results });
|
|
654
687
|
}
|
|
655
688
|
catch (err) {
|
|
@@ -691,9 +724,9 @@ async function startServer(options = {}) {
|
|
|
691
724
|
limit,
|
|
692
725
|
noStrengthen: true,
|
|
693
726
|
// #203: project + mission_domains are SOFT affinity (zero-boost
|
|
694
|
-
// neutral), threaded into computeScore. privacy
|
|
727
|
+
// neutral), threaded into computeScore. 0.16.x: privacy is no
|
|
728
|
+
// longer threaded (vestigial column, never filtered).
|
|
695
729
|
project: filters?.project,
|
|
696
|
-
privacy: filters?.privacy,
|
|
697
730
|
missionDomains: filters?.mission_domains,
|
|
698
731
|
queryEmbedding: queryVec,
|
|
699
732
|
});
|
|
@@ -702,17 +735,18 @@ async function startServer(options = {}) {
|
|
|
702
735
|
}, req.body);
|
|
703
736
|
res.status(r.status).json(r.body);
|
|
704
737
|
});
|
|
705
|
-
// REST /memory?id=
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
738
|
+
// REST /memory?id= — fetch one memory's full content (lazy-load counterpart
|
|
739
|
+
// of /recall-index for REST clients: Hermes/OC plugins). Marks it as used.
|
|
740
|
+
// Prefix ids resolve. 0.16.x: the `privacy` query param is accepted but
|
|
741
|
+
// ignored (column is vestigial, never filtered). Logic in handleMemoryGet.
|
|
709
742
|
app.get("/memory", (req, res) => {
|
|
710
743
|
if (!db) {
|
|
711
744
|
res.status(503).json({ error: "Server not initialized" });
|
|
712
745
|
return;
|
|
713
746
|
}
|
|
747
|
+
warnDeprecatedPrivacyParamIfPresent(req.query, "memory");
|
|
714
748
|
try {
|
|
715
|
-
const r = (0, recall_index_js_1.handleMemoryGet)(db, { id: req.query.id
|
|
749
|
+
const r = (0, recall_index_js_1.handleMemoryGet)(db, { id: req.query.id });
|
|
716
750
|
res.status(r.status).json(r.body);
|
|
717
751
|
}
|
|
718
752
|
catch (err) {
|
|
@@ -727,12 +761,11 @@ async function startServer(options = {}) {
|
|
|
727
761
|
}
|
|
728
762
|
const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
|
|
729
763
|
// No hardcoded default: absent limit → config-driven (recentLimit).
|
|
764
|
+
// 0.16.x: `privacy` query param accepted but ignored (vestigial column).
|
|
730
765
|
const limit = req.query.limit ? Number(req.query.limit) : undefined;
|
|
731
|
-
|
|
732
|
-
? req.query.privacy.split(",").map((s) => s.trim()).filter(Boolean)
|
|
733
|
-
: undefined;
|
|
766
|
+
warnDeprecatedPrivacyParamIfPresent(req.query, "recent");
|
|
734
767
|
try {
|
|
735
|
-
const results = retrieval.searchRecent(db, { project, limit
|
|
768
|
+
const results = retrieval.searchRecent(db, { project, limit });
|
|
736
769
|
res.json({ results });
|
|
737
770
|
}
|
|
738
771
|
catch (err) {
|
|
@@ -806,7 +839,7 @@ async function startServer(options = {}) {
|
|
|
806
839
|
res.status(503).json({ error: "No LLM configured — run npx @gamaze/hicortex init. Session will be retried." });
|
|
807
840
|
return;
|
|
808
841
|
}
|
|
809
|
-
const { text, messages, source_agent, project, session_id, segment_id, session_date, privacy } = req.body ?? {};
|
|
842
|
+
const { text, messages, source_agent, source_agent_id, source_domain, project, session_id, segment_id, session_date, privacy } = req.body ?? {};
|
|
810
843
|
// Resolve the conversation text from either the pre-denoised string or raw messages array.
|
|
811
844
|
let conversationText;
|
|
812
845
|
if (typeof text === "string" && text.length > 0) {
|
|
@@ -897,12 +930,18 @@ async function startServer(options = {}) {
|
|
|
897
930
|
for (const { entry, embedding, i } of toStore) {
|
|
898
931
|
out.push(storage.insertMemory(db, entry, embedding, {
|
|
899
932
|
sourceAgent: source_agent ?? "unknown",
|
|
933
|
+
// Attribution + provenance only (0.16.x): client-declared, never
|
|
934
|
+
// filtered. Default null for older clients that don't send them.
|
|
935
|
+
sourceAgentId: typeof source_agent_id === "string" ? source_agent_id : null,
|
|
936
|
+
sourceDomain: typeof source_domain === "string" ? source_domain : null,
|
|
900
937
|
// Per-chunk key: "<session_id>[#<segment_id>]#<i>". The prefix
|
|
901
938
|
// matches the dedup checks above, so a re-run is idempotent.
|
|
902
939
|
sourceSession: sourcePrefix ? `${sourcePrefix}#${i}` : undefined,
|
|
903
940
|
project: project ?? undefined,
|
|
904
941
|
memoryType: "episode",
|
|
905
|
-
|
|
942
|
+
// 0.16.x: privacy defaults to null (vestigial column). A legacy
|
|
943
|
+
// client that sends an explicit value is honored; absent → null.
|
|
944
|
+
privacy: typeof privacy === "string" ? privacy : null,
|
|
906
945
|
createdAt,
|
|
907
946
|
}));
|
|
908
947
|
}
|
|
@@ -1249,17 +1288,25 @@ async function startServer(options = {}) {
|
|
|
1249
1288
|
const resolveMemoryId = storage.resolveMemoryId;
|
|
1250
1289
|
/**
|
|
1251
1290
|
* Read ~/.hicortex/config.json (persisted by init with LLM and license config).
|
|
1291
|
+
* Routes through loadConfigStrict: a malformed existing file (bad JSON /
|
|
1292
|
+
* non-object / unreadable) emits a visible WARN then fails-soft to null; an
|
|
1293
|
+
* absent file (ENOENT) silently returns null. Without this routing the
|
|
1294
|
+
* agentId self-heal's throw would be unreachable from boot (the old swallow
|
|
1295
|
+
* → null → `if (savedConfig)` guard skipped it).
|
|
1252
1296
|
*/
|
|
1253
1297
|
function readConfigFile(stateDir) {
|
|
1298
|
+
const configPath = (0, node_path_1.join)(stateDir, "config.json");
|
|
1299
|
+
let loaded;
|
|
1254
1300
|
try {
|
|
1255
|
-
|
|
1256
|
-
const { join } = require("node:path");
|
|
1257
|
-
const configPath = join(stateDir, "config.json");
|
|
1258
|
-
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1301
|
+
loaded = (0, init_js_1.loadConfigStrict)(configPath);
|
|
1259
1302
|
}
|
|
1260
|
-
catch {
|
|
1303
|
+
catch (e) {
|
|
1304
|
+
console.warn(`[hicortex] ${configPath} exists but could not be parsed — server booting degraded ` +
|
|
1305
|
+
`(config-driven LLM/decay/recall knobs and agentId self-heal will not apply). ` +
|
|
1306
|
+
`Fix the JSON and restart. Cause: ${e instanceof Error ? e.message : String(e)}`);
|
|
1261
1307
|
return null;
|
|
1262
1308
|
}
|
|
1309
|
+
return loaded.hadFile ? loaded.config : null;
|
|
1263
1310
|
}
|
|
1264
1311
|
/**
|
|
1265
1312
|
* Self-heal: if the daemon plist/systemd unit has a pinned version
|
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 {
|
|
@@ -202,6 +215,17 @@ async function runNightly(options = {}) {
|
|
|
202
215
|
(0, state_js_1.migrateLegacyState)(stateDir);
|
|
203
216
|
// Check mode: client or server
|
|
204
217
|
const savedConfig = readNightlyConfig(stateDir);
|
|
218
|
+
// 0.16.2 activation gap: pre-0.16.2 installs never re-run init, so their
|
|
219
|
+
// config has no agentId → capture sent source_agent_id: null forever (the
|
|
220
|
+
// provenance feature was inert for the whole existing fleet). Self-heal on
|
|
221
|
+
// the first nightly after upgrade: ensureAndPersistAgentId generates + writes
|
|
222
|
+
// the id once (idempotent thereafter). Mutate the in-memory savedConfig so
|
|
223
|
+
// BOTH capture paths (server line below, client via runClientNightly's param)
|
|
224
|
+
// read the value without re-reading the file.
|
|
225
|
+
if (savedConfig) {
|
|
226
|
+
const { agentId } = (0, init_js_1.ensureAndPersistAgentId)((0, node_path_1.join)(stateDir, "config.json"));
|
|
227
|
+
savedConfig.agentId = agentId;
|
|
228
|
+
}
|
|
205
229
|
if (savedConfig?.mode === "client") {
|
|
206
230
|
// --capture-only is accepted in client mode but irrelevant: client nightly
|
|
207
231
|
// is already capture-only (no consolidation step).
|
|
@@ -308,10 +332,14 @@ async function runNightly(options = {}) {
|
|
|
308
332
|
}
|
|
309
333
|
// Step 2: pack each session's delta into ≤60K segments and POST to the
|
|
310
334
|
// local daemon via /distill; cursors advance on confirmed success.
|
|
335
|
+
// source_agent_id / source_domain are per-client provenance from
|
|
336
|
+
// config.json (agentId / sourceDomain) — attribution only, no filtering.
|
|
311
337
|
const result = await (0, capture_js_1.captureBatches)(batches, {
|
|
312
338
|
post: makeLocalPost(port),
|
|
313
339
|
cursorStore,
|
|
314
340
|
dryRun,
|
|
341
|
+
sourceAgentId: savedConfig?.agentId,
|
|
342
|
+
sourceDomain: savedConfig?.sourceDomain,
|
|
315
343
|
});
|
|
316
344
|
memoriesIngested = result.memoriesIngested;
|
|
317
345
|
// A 429/401 stop must hold the watermark too (fix 1): the loop abandoned
|
|
@@ -529,6 +557,10 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
|
|
|
529
557
|
post: makeRemotePost(serverUrl, authToken),
|
|
530
558
|
cursorStore,
|
|
531
559
|
dryRun,
|
|
560
|
+
// Per-client provenance from config.json (agentId / sourceDomain). The
|
|
561
|
+
// server stores these alongside source_agent; nothing filters on them.
|
|
562
|
+
sourceAgentId: config.agentId,
|
|
563
|
+
sourceDomain: config.sourceDomain,
|
|
532
564
|
});
|
|
533
565
|
memoriesIngested = result.memoriesIngested;
|
|
534
566
|
sessionsSent = result.sessionsSent;
|
package/dist/nofit.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ export declare function resolveNoFit(db: Database.Database, memoryId: string, do
|
|
|
90
90
|
* primary derives naturally inside setMemoryTags). Logged distinctly so
|
|
91
91
|
* weak primaries are auditable apart from LLM-tagged rows.
|
|
92
92
|
*/
|
|
93
|
-
export declare function applyWeakPrimary(db: Database.Database, memoryId: string, domain: string, weight: number
|
|
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
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -105,28 +105,40 @@ 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
|
|
|
113
111
|
### Facts Learned
|
|
114
|
-
- [
|
|
112
|
+
- [SUBJECT]: [fact] — [context/source] (${date})
|
|
115
113
|
|
|
116
114
|
### Problems & Solutions
|
|
117
|
-
- [problem] → [solution that worked] (${date})
|
|
115
|
+
- [SUBJECT]: [problem] → [solution that worked] (${date})
|
|
118
116
|
|
|
119
117
|
### Project State Changes
|
|
120
|
-
- [what changed]
|
|
118
|
+
- [SUBJECT]: [what changed], [from → to] (${date})
|
|
121
119
|
|
|
122
120
|
### Key Entities & Relationships
|
|
123
121
|
- [entity A] → [relationship] → [entity B] (${date})
|
|
124
122
|
|
|
125
123
|
### Corrections & Rejections
|
|
126
|
-
- [what AI proposed] → [why rejected/corrected] → [what user wanted instead] (${date})
|
|
124
|
+
- [SUBJECT]: [what AI proposed] → [why rejected/corrected] → [what user wanted instead] (${date})
|
|
127
125
|
(Include: tool use denials, "no/wrong/redo", style feedback, approach rejections,
|
|
128
126
|
user corrections of AI assumptions, quality complaints like "too verbose")
|
|
129
127
|
|
|
128
|
+
TOPIC-FIRST RULE (critical — read carefully):
|
|
129
|
+
Every item MUST begin with its [SUBJECT]: the concrete thing it is about — the
|
|
130
|
+
system, file, component, decision area, or entity. The subject is what a future
|
|
131
|
+
reader would search for.
|
|
132
|
+
- Write: "Electrical load calculation: don't bundle unknown loads into one figure — user rejected the estimate"
|
|
133
|
+
- NOT: "User rejected AI's bundling of unknown loads"
|
|
134
|
+
- Write: "Nightly capture (Hermes): cron sessions are excluded — source='cron' is skipped before distillation"
|
|
135
|
+
- NOT: "Discovered that cron sessions are filtered out"
|
|
136
|
+
Reason: each item's first words become the memory's one-line index entry AND
|
|
137
|
+
dominate its search embedding. An item that opens with a category label, a
|
|
138
|
+
sentiment ("Strong Negative"), or "User rejected…" is unfindable — it matches
|
|
139
|
+
every emotionally-similar prompt and no topically-relevant one. Front-load the
|
|
140
|
+
subject; put reaction, intensity and reasoning AFTER it.
|
|
141
|
+
|
|
130
142
|
RULES:
|
|
131
143
|
- Extract MAX 20 items total (quality over quantity)
|
|
132
144
|
- Each must be useful if recalled in a future session
|
|
@@ -136,12 +148,9 @@ RULES:
|
|
|
136
148
|
- PRIORITIZE Corrections & Rejections — these are high-value signals for learning
|
|
137
149
|
what the user does NOT want. Even a single "no" or style correction is worth extracting.
|
|
138
150
|
- Strong language or profanity from the user is a high-intensity signal — it indicates
|
|
139
|
-
the correction matters deeply. Note the intensity
|
|
140
|
-
-
|
|
141
|
-
|
|
142
|
-
- WORK: project-specific decisions, architecture choices, client/business context
|
|
143
|
-
- PERSONAL: personal preferences, family, health, lifestyle, private life
|
|
144
|
-
- SENSITIVE: API keys mentioned, credentials, financial account details, medical records
|
|
151
|
+
the correction matters deeply. Note the intensity AFTER the subject, never before it
|
|
152
|
+
(e.g. "Pricing tiers: strongly rejected per-agent billing — …", not
|
|
153
|
+
"[Strong Negative] User rejected per-agent billing"). The subject always comes first.
|
|
145
154
|
- Omit any section that has zero items (don't include empty sections)
|
|
146
155
|
- If nothing worth extracting, output ONLY: "NO_EXTRACT"
|
|
147
156
|
`;
|
package/dist/recall-index.d.ts
CHANGED
|
@@ -25,13 +25,31 @@ import { SessionRecallRegistry } from "./recall-registry.js";
|
|
|
25
25
|
export interface RecallIndexOptions {
|
|
26
26
|
/** Minimum measured cosine for vector-only candidates (config
|
|
27
27
|
* `recallMinSimilarity`). FTS-matched candidates pass regardless — a BM25
|
|
28
|
-
* text match is direct evidence of relevance. Default 0.
|
|
29
|
-
*
|
|
28
|
+
* text match is direct evidence of relevance. Default 0.62 (raised from 0.55
|
|
29
|
+
* on 2026-08-03 per a 0.01-step floor sweep on the rewritten corpus): steady
|
|
30
|
+
* ~3:1 noise:signal removal with no knee; 0.62 = +2.2pts precision, 10/98
|
|
31
|
+
* prompts silent, sits below the 0.63 local pessimum. The floor is a noise
|
|
32
|
+
* dial, NOT a silence mechanism — at 0.62 each correctly-silenced empty prompt
|
|
33
|
+
* comes with ~1.5 wrongly-silenced (real signal); a non-cosine gate is the
|
|
34
|
+
* real silence fix (eval #3 §4). */
|
|
30
35
|
minSimilarity?: number;
|
|
31
|
-
/** Max index lines per response (config `recallMaxItems`). Default
|
|
36
|
+
/** Max index lines per response (config `recallMaxItems`). Default 5
|
|
37
|
+
* (lowered from 6 on 2026-08-03). Per-slot decomposition at floor 0.62:
|
|
38
|
+
* slot 6 gives NO prompt its first relevant memory — "6 is wrong" is the
|
|
39
|
+
* robust, prompt-set-independent finding, and 5 captures it. The K-sweep
|
|
40
|
+
* is monotone (precision@4 33.7% > @6 30.6% > @8 28.3%), so 4 is
|
|
41
|
+
* lower-noise — but the 4-vs-5 distinction rests on 5 of 98 prompts and is
|
|
42
|
+
* overfitting-fragile (K and the floor were tuned on the same set); 5 hedges
|
|
43
|
+
* with coverage at modest cost. Lower to 4 if a fresh-prompt eval replicates. */
|
|
32
44
|
maxItems?: number;
|
|
33
45
|
/** Prompts shorter than this are skipped (continuations, "yes", "do it"). */
|
|
34
46
|
minPromptLength?: number;
|
|
47
|
+
/** Max chars of the memory's first line shown in an index entry (config
|
|
48
|
+
* `recallTitleChars`). Default 100 (reverted from 150 on 2026-08-03): the
|
|
49
|
+
* full-corpus relevance eval (#3, §5) found 100 vs 150 statistically
|
|
50
|
+
* identical (0.6pts apart, N=40, full CI overlap); 100 saves ~13% tokens
|
|
51
|
+
* per block. */
|
|
52
|
+
titleChars?: number;
|
|
35
53
|
}
|
|
36
54
|
export interface RecallIndexResult {
|
|
37
55
|
status: number;
|
|
@@ -39,20 +57,42 @@ export interface RecallIndexResult {
|
|
|
39
57
|
}
|
|
40
58
|
/** First content line, de-markdowned and truncated — the index line title. */
|
|
41
59
|
export declare function memoryTitle(content: string, maxLen?: number): string;
|
|
42
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Render one production index line. Exported (2026-08-02, relevance eval #v2)
|
|
62
|
+
* so the eval can measure the REAL rendered surface instead of reimplementing
|
|
63
|
+
* it — `maxLen` threads through to `memoryTitle` unchanged (default
|
|
64
|
+
* DEFAULT_TITLE_CHARS = 100, config `recallTitleChars`) so the eval's snippet-length
|
|
65
|
+
* sweep (spec §4.2) can call this SAME function at 100/150/title1sent without
|
|
66
|
+
* duplicating the date/scope/agent/type meta-line logic.
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatIndexLine(r: MemorySearchResult & {
|
|
69
|
+
domain?: string | null;
|
|
70
|
+
}, maxLen?: number): string;
|
|
71
|
+
/**
|
|
72
|
+
* Relevance gate: a real BM25 text match (FTS) passes unconditionally; a
|
|
73
|
+
* vector-only candidate must clear `minSimilarity`.
|
|
74
|
+
*
|
|
75
|
+
* NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
|
|
76
|
+
* weight toward FTS-sourced entries. In practice FTS is currently inert on
|
|
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
|
|
80
|
+
* (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
|
|
81
|
+
*/
|
|
43
82
|
export declare function passesRelevanceGate(r: MemorySearchResult, minSimilarity: number): boolean;
|
|
44
83
|
/** Recall filters a client may push per request (#193 review F1): a scoped
|
|
45
|
-
* plugin (Hermes
|
|
84
|
+
* plugin (Hermes default_project / mission_domains) must be able to narrow
|
|
46
85
|
* recall exactly like the legacy /search prefetch did — dropping them
|
|
47
86
|
* silently would leak out-of-scope memory titles into the injected index.
|
|
48
87
|
*
|
|
49
|
-
* #203: `project` and `mission_domains` are
|
|
50
|
-
* retrieval (zero-boost neutral, never a filter / penalty)
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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. */
|
|
53
94
|
export interface RecallFilters {
|
|
54
95
|
project?: string;
|
|
55
|
-
privacy?: string[];
|
|
56
96
|
/** #203: Hermes mission domains (declared in plugin config). Soft domain
|
|
57
97
|
* affinity in computeScore via max overlapping memory_tags.weight. */
|
|
58
98
|
mission_domains?: string[];
|
|
@@ -68,14 +108,9 @@ export interface RecallIndexDeps {
|
|
|
68
108
|
}
|
|
69
109
|
/** Normalize a request-supplied string-list param: array of strings or a CSV
|
|
70
110
|
* string → string[] | undefined. Anything else (or an empty result) means
|
|
71
|
-
* "absent" — never a partial guess.
|
|
72
|
-
*
|
|
111
|
+
* "absent" — never a partial guess. Used by `mission_domains` (#203) so it
|
|
112
|
+
* accepts `["A","B"]` and `"A, B"` alike. */
|
|
73
113
|
export declare function parseStringListParam(v: unknown): string[] | undefined;
|
|
74
|
-
/** Normalize a request-supplied privacy filter: array of strings or a CSV
|
|
75
|
-
* string → string[] | undefined. Anything else (or an empty result) means
|
|
76
|
-
* "no filter" — never a partial guess. Delegates to parseStringListParam;
|
|
77
|
-
* kept as a named export for tests and handleMemoryGet callers. */
|
|
78
|
-
export declare function parsePrivacyParam(v: unknown): string[] | undefined;
|
|
79
114
|
/**
|
|
80
115
|
* Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
|
|
81
116
|
* all behavior lives here so tests exercise it directly.
|
|
@@ -88,14 +123,14 @@ export declare function handleRecallIndex(deps: RecallIndexDeps, body: unknown):
|
|
|
88
123
|
*
|
|
89
124
|
* - Short/prefix ids resolve via storage.resolveMemoryId (F6) — the 8-char
|
|
90
125
|
* citation ids agents are taught must work here like on /update, /delete.
|
|
91
|
-
* - Optional `privacy` filter (array or CSV): when present and the memory's
|
|
92
|
-
* privacy level is not in the allowed set, respond 404 with the SAME
|
|
93
|
-
* not-found message — a scoped client must not learn the memory exists.
|
|
94
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.
|
|
95
131
|
*/
|
|
96
132
|
export declare function handleMemoryGet(db: Database.Database, query: {
|
|
97
133
|
id?: unknown;
|
|
98
|
-
privacy?: unknown;
|
|
99
134
|
}): RecallIndexResult;
|
|
100
135
|
/**
|
|
101
136
|
* MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
|