@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
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* embedding of the domain's config description instead.
|
|
13
13
|
* - weight(memory, tag) = cosine(memory embedding, prototype(tag)). Both
|
|
14
14
|
* vectors are L2-normalized, so cosine reduces to a dot product.
|
|
15
|
-
* - PRIMARY (memories.domain) = argmax-weight tag,
|
|
16
|
-
*
|
|
17
|
-
* the owner's Work firewall). Fully mechanical, no LLM.
|
|
15
|
+
* - PRIMARY (memories.domain) = argmax-weight tag, with LLM tag order
|
|
16
|
+
* breaking exact-weight ties. Fully mechanical, no LLM.
|
|
18
17
|
*
|
|
19
18
|
* The LLM decides ONLY the discrete part (which schemas apply — see
|
|
20
19
|
* domain-classify.ts); ALL gradation is derived from embeddings here.
|
|
@@ -30,7 +29,6 @@ exports.blobToVec = blobToVec;
|
|
|
30
29
|
exports.l2Normalize = l2Normalize;
|
|
31
30
|
exports.weightedAdd = weightedAdd;
|
|
32
31
|
exports.tagWeight = tagWeight;
|
|
33
|
-
exports.compartmentSet = compartmentSet;
|
|
34
32
|
exports.derivePrimary = derivePrimary;
|
|
35
33
|
exports.loadDomainPrototypes = loadDomainPrototypes;
|
|
36
34
|
exports.computeDomainPrototypes = computeDomainPrototypes;
|
|
@@ -107,33 +105,23 @@ function tagWeight(memoryEmbedding, prototype) {
|
|
|
107
105
|
dot += memoryEmbedding[i] * prototype[i];
|
|
108
106
|
return dot;
|
|
109
107
|
}
|
|
110
|
-
/** The configured compartment domain names (DomainDef.compartment === true). */
|
|
111
|
-
function compartmentSet(domains) {
|
|
112
|
-
return new Set(domains.filter((d) => d.compartment === true).map((d) => d.name));
|
|
113
|
-
}
|
|
114
108
|
/**
|
|
115
109
|
* Derive the PRIMARY tag (memories.domain) from a weighted tag set.
|
|
116
110
|
*
|
|
117
111
|
* Rules (deterministic, no LLM):
|
|
118
|
-
* 1.
|
|
119
|
-
* (unusual) case of several arises.
|
|
120
|
-
* 2. Else the argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
112
|
+
* 1. The argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
121
113
|
* order: ties (and all-null weights) resolve to the EARLIEST array
|
|
122
114
|
* position — strict `>` comparison keeps the first maximum.
|
|
123
|
-
*
|
|
115
|
+
* 2. A null weight loses to any numeric weight (treated as -Infinity).
|
|
124
116
|
*
|
|
125
117
|
* Throws on an empty tag set — callers guarantee >= 1 tag (an empty tag set
|
|
126
118
|
* from the classifier is a NO-FIT and must be routed through nofit.ts, never
|
|
127
119
|
* here); an empty set reaching this function is a programming error.
|
|
128
120
|
*/
|
|
129
|
-
function derivePrimary(tags
|
|
121
|
+
function derivePrimary(tags) {
|
|
130
122
|
if (tags.length === 0) {
|
|
131
123
|
throw new Error("derivePrimary: empty tag set (callers must pass >= 1 tag)");
|
|
132
124
|
}
|
|
133
|
-
for (const t of tags) {
|
|
134
|
-
if (compartments.has(t.tag))
|
|
135
|
-
return t.tag;
|
|
136
|
-
}
|
|
137
125
|
let best = tags[0];
|
|
138
126
|
let bestWeight = best.weight ?? Number.NEGATIVE_INFINITY;
|
|
139
127
|
for (let i = 1; i < tags.length; i++) {
|
|
@@ -313,15 +301,18 @@ function recomputeAllTagWeights(db, prototypes) {
|
|
|
313
301
|
}
|
|
314
302
|
/**
|
|
315
303
|
* Re-derive the PRIMARY (memories.domain) of every tagged memory from its
|
|
316
|
-
* current tag weights:
|
|
317
|
-
*
|
|
318
|
-
*
|
|
304
|
+
* current tag weights: argmax weight, LLM order (memory_tags insertion order =
|
|
305
|
+
* rowid, written most-relevant-first by storage.setMemoryTags) breaking
|
|
306
|
+
* exact-weight ties.
|
|
319
307
|
*
|
|
320
308
|
* Memories with NO memory_tags rows are untouched (e.g. infra-skipped rows
|
|
321
309
|
* awaiting classification — issue #150 discipline).
|
|
322
310
|
*/
|
|
323
311
|
function refreshPrimaries(db, domains) {
|
|
324
|
-
|
|
312
|
+
// `domains` is accepted for API symmetry with the other reconsolidation
|
|
313
|
+
// passes (which need prototypes/weights); the primary is now pure argmax
|
|
314
|
+
// and does not depend on the domain set.
|
|
315
|
+
void domains;
|
|
325
316
|
const rows = db
|
|
326
317
|
.prepare(`SELECT mt.memory_id, mt.tag, mt.weight, m.domain
|
|
327
318
|
FROM memory_tags mt JOIN memories m ON m.id = mt.memory_id
|
|
@@ -341,7 +332,7 @@ function refreshPrimaries(db, domains) {
|
|
|
341
332
|
let updated = 0;
|
|
342
333
|
const tx = db.transaction(() => {
|
|
343
334
|
for (const [memoryId, entry] of byMemory) {
|
|
344
|
-
const primary = derivePrimary(entry.tags
|
|
335
|
+
const primary = derivePrimary(entry.tags);
|
|
345
336
|
if (primary !== entry.domain) {
|
|
346
337
|
update.run(primary, memoryId);
|
|
347
338
|
updated++;
|
package/dist/seed-lesson.js
CHANGED
package/dist/storage.d.ts
CHANGED
|
@@ -54,11 +54,6 @@ export interface SetMemoryTagsOptions {
|
|
|
54
54
|
* (repaired by the next nightly recompute).
|
|
55
55
|
*/
|
|
56
56
|
weights?: Record<string, number | null>;
|
|
57
|
-
/**
|
|
58
|
-
* Compartment domain names (DomainDef.compartment === true): a tagged
|
|
59
|
-
* compartment domain becomes the primary regardless of weights.
|
|
60
|
-
*/
|
|
61
|
-
compartments?: Set<string>;
|
|
62
57
|
}
|
|
63
58
|
/**
|
|
64
59
|
* Set a memory's classification tags (graded schema model).
|
|
@@ -69,9 +64,8 @@ export interface SetMemoryTagsOptions {
|
|
|
69
64
|
* row stores its association weight (NULL when not yet computed).
|
|
70
65
|
*
|
|
71
66
|
* The PRIMARY (memories.domain) is DERIVED here — never passed in by the LLM:
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* weights never diverge.
|
|
67
|
+
* argmax weight, else first tag (all-null weights). The whole update is one
|
|
68
|
+
* transaction so domain, tag set, and weights never diverge.
|
|
75
69
|
*
|
|
76
70
|
* @returns the derived primary written to memories.domain
|
|
77
71
|
*/
|
|
@@ -149,9 +143,10 @@ export declare function getBm25Weights(): Bm25Weights;
|
|
|
149
143
|
*
|
|
150
144
|
* `project` is NOT a filter here (#203): the hard project WHERE from #192 was
|
|
151
145
|
* removed — project is now a soft affinity boost in retrieval.computeScore AND
|
|
152
|
-
* a weighted field in BM25F (#205). `privacy`
|
|
153
|
-
*
|
|
154
|
-
*
|
|
146
|
+
* a weighted field in BM25F (#205). `privacy` is NOT a filter (0.16.x: the
|
|
147
|
+
* column is fully vestigial — stored, never filtered; the privacy IN-clause
|
|
148
|
+
* was removed). `sourceAgent` stays a hard filter (kept for completeness; no
|
|
149
|
+
* production caller of retrieve() currently passes it).
|
|
155
150
|
*
|
|
156
151
|
* #205 sign handling: FTS5's `bm25(table, w0, w1, …)` returns a NEGATIVE score
|
|
157
152
|
* where MORE-negative = better match (it is 1 − the normalized BM25 score,
|
|
@@ -162,7 +157,7 @@ export declare function getBm25Weights(): Bm25Weights;
|
|
|
162
157
|
* positionally as parameters (NOT string-interpolated) so query-planner
|
|
163
158
|
* caching is unaffected and the config path is the only editor.
|
|
164
159
|
*/
|
|
165
|
-
export declare function searchFts(db: Database.Database, query: string, limit?: number,
|
|
160
|
+
export declare function searchFts(db: Database.Database, query: string, limit?: number, sourceAgent?: string): Array<Memory & {
|
|
166
161
|
rank: number;
|
|
167
162
|
}>;
|
|
168
163
|
/**
|
|
@@ -184,6 +179,8 @@ export declare function insertMemoriesBatch(db: Database.Database, memories: Arr
|
|
|
184
179
|
content: string;
|
|
185
180
|
embedding: Float32Array;
|
|
186
181
|
sourceAgent?: string;
|
|
182
|
+
sourceAgentId?: string | null;
|
|
183
|
+
sourceDomain?: string | null;
|
|
187
184
|
sourceSession?: string | null;
|
|
188
185
|
project?: string | null;
|
|
189
186
|
privacy?: string;
|
package/dist/storage.js
CHANGED
|
@@ -69,10 +69,10 @@ function insertMemory(db, content, embedding, opts = {}) {
|
|
|
69
69
|
const result = db
|
|
70
70
|
.prepare(`INSERT OR IGNORE INTO memories
|
|
71
71
|
(id, content, base_strength, last_accessed, access_count,
|
|
72
|
-
created_at, ingested_at, source_agent,
|
|
73
|
-
privacy, memory_type)
|
|
74
|
-
VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?)`)
|
|
75
|
-
.run(id, content, opts.baseStrength ?? 0.5, ts, ts, ingestedTs, opts.sourceAgent ?? "default", sourceSession, opts.project ?? null, opts.privacy ??
|
|
72
|
+
created_at, ingested_at, source_agent, source_agent_id, source_session,
|
|
73
|
+
source_domain, project, privacy, memory_type)
|
|
74
|
+
VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
75
|
+
.run(id, content, opts.baseStrength ?? 0.5, ts, ts, ingestedTs, opts.sourceAgent ?? "default", opts.sourceAgentId ?? null, sourceSession, opts.sourceDomain ?? null, opts.project ?? null, opts.privacy ?? null, opts.memoryType ?? "episode");
|
|
76
76
|
if (result.changes > 0) {
|
|
77
77
|
// New row — store its vector.
|
|
78
78
|
db.prepare("INSERT INTO memory_vectors (id, embedding) VALUES (?, ?)").run(id, embedToBlob(embedding));
|
|
@@ -192,9 +192,8 @@ function deleteMemory(db, memoryId) {
|
|
|
192
192
|
* row stores its association weight (NULL when not yet computed).
|
|
193
193
|
*
|
|
194
194
|
* The PRIMARY (memories.domain) is DERIVED here — never passed in by the LLM:
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* weights never diverge.
|
|
195
|
+
* argmax weight, else first tag (all-null weights). The whole update is one
|
|
196
|
+
* transaction so domain, tag set, and weights never diverge.
|
|
198
197
|
*
|
|
199
198
|
* @returns the derived primary written to memories.domain
|
|
200
199
|
*/
|
|
@@ -209,7 +208,7 @@ function setMemoryTags(db, memoryId, tags, options = {}) {
|
|
|
209
208
|
tag,
|
|
210
209
|
weight: options.weights?.[tag] ?? null,
|
|
211
210
|
}));
|
|
212
|
-
const primary = (0, schema_prototypes_js_1.derivePrimary)(weighted
|
|
211
|
+
const primary = (0, schema_prototypes_js_1.derivePrimary)(weighted);
|
|
213
212
|
const setDomain = db.prepare("UPDATE memories SET domain = ? WHERE id = ?");
|
|
214
213
|
const clearTags = db.prepare("DELETE FROM memory_tags WHERE memory_id = ?");
|
|
215
214
|
const insertTag = db.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag, weight) VALUES (?, ?, ?)");
|
|
@@ -352,9 +351,10 @@ function getBm25Weights() {
|
|
|
352
351
|
*
|
|
353
352
|
* `project` is NOT a filter here (#203): the hard project WHERE from #192 was
|
|
354
353
|
* removed — project is now a soft affinity boost in retrieval.computeScore AND
|
|
355
|
-
* a weighted field in BM25F (#205). `privacy`
|
|
356
|
-
*
|
|
357
|
-
*
|
|
354
|
+
* a weighted field in BM25F (#205). `privacy` is NOT a filter (0.16.x: the
|
|
355
|
+
* column is fully vestigial — stored, never filtered; the privacy IN-clause
|
|
356
|
+
* was removed). `sourceAgent` stays a hard filter (kept for completeness; no
|
|
357
|
+
* production caller of retrieve() currently passes it).
|
|
358
358
|
*
|
|
359
359
|
* #205 sign handling: FTS5's `bm25(table, w0, w1, …)` returns a NEGATIVE score
|
|
360
360
|
* where MORE-negative = better match (it is 1 − the normalized BM25 score,
|
|
@@ -365,14 +365,9 @@ function getBm25Weights() {
|
|
|
365
365
|
* positionally as parameters (NOT string-interpolated) so query-planner
|
|
366
366
|
* caching is unaffected and the config path is the only editor.
|
|
367
367
|
*/
|
|
368
|
-
function searchFts(db, query, limit = 10,
|
|
368
|
+
function searchFts(db, query, limit = 10, sourceAgent) {
|
|
369
369
|
const conditions = ["memories_fts MATCH ?"];
|
|
370
370
|
const params = [query];
|
|
371
|
-
if (privacy && privacy.length > 0) {
|
|
372
|
-
const placeholders = privacy.map(() => "?").join(", ");
|
|
373
|
-
conditions.push(`m.privacy IN (${placeholders})`);
|
|
374
|
-
params.push(...privacy);
|
|
375
|
-
}
|
|
376
371
|
if (sourceAgent) {
|
|
377
372
|
conditions.push("m.source_agent = ?");
|
|
378
373
|
params.push(sourceAgent);
|
|
@@ -460,18 +455,21 @@ function deleteLinks(db, memoryId) {
|
|
|
460
455
|
* Batch insert memories. Returns count inserted.
|
|
461
456
|
*/
|
|
462
457
|
function insertMemoriesBatch(db, memories) {
|
|
458
|
+
// privacy default is null (0.16.x: the distiller no longer sets WORK — the
|
|
459
|
+
// column is vestigial, never filtered, and goes NULL unless a caller sends
|
|
460
|
+
// an explicit value).
|
|
463
461
|
const insertMem = db.prepare(`INSERT INTO memories
|
|
464
462
|
(id, content, base_strength, last_accessed, access_count,
|
|
465
|
-
created_at, ingested_at, source_agent,
|
|
466
|
-
privacy, memory_type)
|
|
467
|
-
VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?)`);
|
|
463
|
+
created_at, ingested_at, source_agent, source_agent_id, source_session,
|
|
464
|
+
source_domain, project, privacy, memory_type)
|
|
465
|
+
VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
468
466
|
const insertVec = db.prepare("INSERT INTO memory_vectors (id, embedding) VALUES (?, ?)");
|
|
469
467
|
const tx = db.transaction(() => {
|
|
470
468
|
let count = 0;
|
|
471
469
|
for (const mem of memories) {
|
|
472
470
|
const id = (0, node_crypto_1.randomUUID)();
|
|
473
471
|
const ts = nowIso();
|
|
474
|
-
insertMem.run(id, mem.content, mem.baseStrength ?? 0.5, ts, ts, ts, mem.sourceAgent ?? "default", mem.sourceSession ?? null, mem.project ?? null, mem.privacy ??
|
|
472
|
+
insertMem.run(id, mem.content, mem.baseStrength ?? 0.5, ts, ts, ts, mem.sourceAgent ?? "default", mem.sourceAgentId ?? null, mem.sourceSession ?? null, mem.sourceDomain ?? null, mem.project ?? null, mem.privacy ?? null, mem.memoryType ?? "episode");
|
|
475
473
|
insertVec.run(id, embedToBlob(mem.embedding));
|
|
476
474
|
count++;
|
|
477
475
|
}
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* v — package version
|
|
7
7
|
* pv — payload schema version
|
|
8
8
|
* mode — server or client
|
|
9
|
-
* agent — cc, pi, oc, or mixed (detected from session sources)
|
|
9
|
+
* agent — cc, pi, oc, or mixed (detected from session sources); OMITTED
|
|
10
|
+
* when the agent type is genuinely unknown (pre-flight abort — no
|
|
11
|
+
* transcripts read yet). The admin summary buckets a missing agent
|
|
12
|
+
* as "?", distinct from any real type, so an aborting Hermes/OC
|
|
13
|
+
* client is never miscounted as "cc".
|
|
10
14
|
* mem — total memory count
|
|
11
15
|
* lessons — total lesson count
|
|
12
16
|
* sessions — sessions distilled this run
|
|
@@ -41,7 +45,14 @@ export interface TelemetryPayload {
|
|
|
41
45
|
id: string;
|
|
42
46
|
v: string;
|
|
43
47
|
mode: string;
|
|
44
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Agent type detected from session sources (cc/pi/oc/mixed). OMITTED when
|
|
50
|
+
* unknown — currently only the pre-flight abort path, where no transcripts
|
|
51
|
+
* have been read yet (sending "cc" there mislabelled aborting Hermes/OC
|
|
52
|
+
* clients in the admin aggregate). The admin summary buckets a missing agent
|
|
53
|
+
* as "?", which is the honest signal.
|
|
54
|
+
*/
|
|
55
|
+
agent?: string;
|
|
45
56
|
mem: number;
|
|
46
57
|
lessons: number;
|
|
47
58
|
sessions: number;
|
package/dist/telemetry.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* v — package version
|
|
8
8
|
* pv — payload schema version
|
|
9
9
|
* mode — server or client
|
|
10
|
-
* agent — cc, pi, oc, or mixed (detected from session sources)
|
|
10
|
+
* agent — cc, pi, oc, or mixed (detected from session sources); OMITTED
|
|
11
|
+
* when the agent type is genuinely unknown (pre-flight abort — no
|
|
12
|
+
* transcripts read yet). The admin summary buckets a missing agent
|
|
13
|
+
* as "?", distinct from any real type, so an aborting Hermes/OC
|
|
14
|
+
* client is never miscounted as "cc".
|
|
11
15
|
* mem — total memory count
|
|
12
16
|
* lessons — total lesson count
|
|
13
17
|
* sessions — sessions distilled this run
|
package/dist/types.d.ts
CHANGED
|
@@ -13,9 +13,23 @@ export interface Memory {
|
|
|
13
13
|
ingested_at: string;
|
|
14
14
|
source_agent: string;
|
|
15
15
|
source_session: string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Stable attribution id of the capturing client (a per-install UUID from
|
|
18
|
+
* config.json `agentId`). Survives agent/machine renames — unlike
|
|
19
|
+
* `source_agent` (a readable name). Attribution only; nothing filters on it.
|
|
20
|
+
* NULL on memories captured before this column existed. (0.16.x)
|
|
21
|
+
*/
|
|
22
|
+
source_agent_id: string | null;
|
|
16
23
|
project: string | null;
|
|
17
24
|
domain: string | null;
|
|
18
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Provenance only (0.16.x): the client-declared topic/domain of the
|
|
27
|
+
* capturing agent (config.json `sourceDomain`). NOT used for recall filtering
|
|
28
|
+
* or scoring, and NOT the content-classified primary (that is `domain`
|
|
29
|
+
* above). NULL when the client declares none. Echoed back on /memory GET.
|
|
30
|
+
*/
|
|
31
|
+
source_domain: string | null;
|
|
32
|
+
privacy: ("PUBLIC" | "WORK" | "PERSONAL" | "SENSITIVE") | null;
|
|
19
33
|
memory_type: "episode" | "lesson" | "fact" | "decision";
|
|
20
34
|
updated_at: string | null;
|
|
21
35
|
}
|
|
@@ -40,7 +54,7 @@ export interface MemorySearchResult {
|
|
|
40
54
|
access_count: number;
|
|
41
55
|
memory_type: string;
|
|
42
56
|
project: string | null;
|
|
43
|
-
/** Origin agent (e.g. "hermes/
|
|
57
|
+
/** Origin agent (e.g. "hermes/profile-name", "cc/machine-name") — surfaced in the recall
|
|
44
58
|
* one-liner so agents can calibrate trust (#202 provenance). Optional on the
|
|
45
59
|
* result type (matches how `domain` is threaded) to avoid breaking fixtures. */
|
|
46
60
|
source_agent?: string | null;
|
|
@@ -157,6 +171,15 @@ export interface HicortexConfig {
|
|
|
157
171
|
serverUrl?: string;
|
|
158
172
|
/** Bearer token for the Hicortex server. Localhost bypasses auth by default. */
|
|
159
173
|
authToken?: string;
|
|
174
|
+
/**
|
|
175
|
+
* Stable per-install UUID generated by `init` (see ensureAgentId in init.ts;
|
|
176
|
+
* never rotated). Attribution identity of the capturing client — stored on
|
|
177
|
+
* each captured memory as `source_agent_id` (see Memory.source_agent_id) and
|
|
178
|
+
* sent on every /distill segment. Survives agent/machine renames, unlike the
|
|
179
|
+
* readable `source_agent` name. Pure attribution; nothing filters or scopes
|
|
180
|
+
* on it. (0.16.x)
|
|
181
|
+
*/
|
|
182
|
+
agentId?: string;
|
|
160
183
|
/** @deprecated Use the Hicortex server for distillation and consolidation. */
|
|
161
184
|
llmBaseUrl?: string;
|
|
162
185
|
/** @deprecated Use the Hicortex server for distillation and consolidation. */
|
|
@@ -194,6 +217,21 @@ export interface HicortexConfig {
|
|
|
194
217
|
consolidateHour?: number;
|
|
195
218
|
/** @deprecated The OC plugin no longer opens its own database. */
|
|
196
219
|
dbPath?: string;
|
|
220
|
+
/**
|
|
221
|
+
* Client-declared topic/domain of THIS capturing agent (provenance only,
|
|
222
|
+
* 0.16.x). Sent on captured memories as `source_domain` (see
|
|
223
|
+
* Memory.source_domain) — NOT used for recall filtering or scoring, and NOT
|
|
224
|
+
* the content-classified primary (which is the server-derived `domain`
|
|
225
|
+
* column on each memory, classified against `domains` below).
|
|
226
|
+
*
|
|
227
|
+
* DISTINCT from `domains` (plural) directly below: `domains` is the
|
|
228
|
+
* config-owned VOCABULARY — the server's life-sphere list that memories are
|
|
229
|
+
* content-classified against; this singular `sourceDomain` is the client
|
|
230
|
+
* declaring "I am an agent that works on topic X", recorded as provenance on
|
|
231
|
+
* what it captures. Do not conflate the two. (Renamed from `domain` in
|
|
232
|
+
* 0.16.x — one char from `domains`, meant something unrelated.)
|
|
233
|
+
*/
|
|
234
|
+
sourceDomain?: string;
|
|
197
235
|
/**
|
|
198
236
|
* Optional config-owned domain list — the user's top-level memory spheres
|
|
199
237
|
* (life areas OR project/topic areas). When present, the nightly multi-tag
|
|
@@ -205,7 +243,7 @@ export interface HicortexConfig {
|
|
|
205
243
|
* Server-mode `init` scaffolds a generic 5-domain default (Work, Personal,
|
|
206
244
|
* People, Health, Finance — see GENERIC_DEFAULT_DOMAINS in init.ts) when
|
|
207
245
|
* this key is absent, and NEVER touches an existing list. A power-user
|
|
208
|
-
* example (
|
|
246
|
+
* example (custom weakPrimaryFloor) ships as
|
|
209
247
|
* domains.example.json in the package root.
|
|
210
248
|
*
|
|
211
249
|
* NO fallback bucket is needed or special-cased (owner amendment 07.07):
|
|
@@ -222,18 +260,33 @@ export interface HicortexConfig {
|
|
|
222
260
|
* near its lower tail). See domains.example.json for a worked example.
|
|
223
261
|
*/
|
|
224
262
|
weakPrimaryFloor?: number;
|
|
263
|
+
/**
|
|
264
|
+
* Per-attempt timeout (ms) for the CLIENT nightly's pre-flight GET /health
|
|
265
|
+
* check before capturing (#163). Default 15000. Overridable per machine —
|
|
266
|
+
* a wired Pi vs a sleeping laptop want different values. See runClientNightly
|
|
267
|
+
* in nightly.ts. No effect in server mode (server capture is localhost).
|
|
268
|
+
*/
|
|
269
|
+
preflightTimeoutMs?: number;
|
|
270
|
+
/**
|
|
271
|
+
* Max attempts for the client nightly's pre-flight /health retry loop (#163).
|
|
272
|
+
* Default 3. Attempts are spaced preflightRetryGapMs apart; on exhaustion the
|
|
273
|
+
* run aborts with a non-zero exit code and an ok=false telemetry ping so the
|
|
274
|
+
* failure is visible to systemd/launchd and the activity aggregate.
|
|
275
|
+
*/
|
|
276
|
+
preflightAttempts?: number;
|
|
277
|
+
/**
|
|
278
|
+
* Gap (ms) between pre-flight /health attempts in the client nightly (#163).
|
|
279
|
+
* Default 60000. Wall-clock-optimistic on a sleeping laptop — setTimeout does
|
|
280
|
+
* NOT advance while macOS is asleep, so real elapsed time can exceed the
|
|
281
|
+
* nominal worst case. Not a defect (capture lock isn't held; cursor design is
|
|
282
|
+
* dup-over-loss); just don't treat the nominal sum as a hard bound.
|
|
283
|
+
*/
|
|
284
|
+
preflightRetryGapMs?: number;
|
|
225
285
|
}
|
|
226
286
|
/** A config-owned life-sphere domain (see HicortexConfig.domains). */
|
|
227
287
|
export interface DomainDef {
|
|
228
288
|
name: string;
|
|
229
289
|
description: string;
|
|
230
|
-
/**
|
|
231
|
-
* Deliberate compartmentalization (graded-schema spec, 07.07.2026): when
|
|
232
|
-
* true, this domain becomes the PRIMARY (memories.domain) whenever it is
|
|
233
|
-
* tagged, overriding the argmax-weight rule. The owner's config flags only
|
|
234
|
-
* Work — a work/life firewall. Optional; absent = false.
|
|
235
|
-
*/
|
|
236
|
-
compartment?: boolean;
|
|
237
290
|
}
|
|
238
291
|
/** Response from license validation API. */
|
|
239
292
|
export interface LicenseInfo {
|
|
@@ -286,8 +339,14 @@ export interface ModuleIndex {
|
|
|
286
339
|
export interface InsertMemoryOptions {
|
|
287
340
|
sourceAgent?: string;
|
|
288
341
|
sourceSession?: string | null;
|
|
342
|
+
/** Stable client UUID (config.json `agentId`). Attribution only. */
|
|
343
|
+
sourceAgentId?: string | null;
|
|
344
|
+
/** Client-declared topic/domain of the capturing agent. Provenance only. */
|
|
345
|
+
sourceDomain?: string | null;
|
|
289
346
|
project?: string | null;
|
|
290
|
-
|
|
347
|
+
/** 0.16.x: vestigial — stored but never filtered. null (or absent) when the
|
|
348
|
+
* caller doesn't declare one; an explicit value is honored as-is. */
|
|
349
|
+
privacy?: string | null;
|
|
291
350
|
memoryType?: string;
|
|
292
351
|
baseStrength?: number;
|
|
293
352
|
createdAt?: string;
|
|
@@ -297,16 +356,3 @@ export interface VectorSearchOptions {
|
|
|
297
356
|
limit?: number;
|
|
298
357
|
excludeIds?: string[];
|
|
299
358
|
}
|
|
300
|
-
/** Options for FTS search. */
|
|
301
|
-
export interface FtsSearchOptions {
|
|
302
|
-
limit?: number;
|
|
303
|
-
privacy?: string[];
|
|
304
|
-
sourceAgent?: string;
|
|
305
|
-
}
|
|
306
|
-
/** Options for retrieval. */
|
|
307
|
-
export interface RetrievalOptions {
|
|
308
|
-
limit?: number;
|
|
309
|
-
project?: string | null;
|
|
310
|
-
privacy?: string[];
|
|
311
|
-
sourceAgent?: string;
|
|
312
|
-
}
|
package/domains.example.json
CHANGED
|
@@ -33,14 +33,13 @@
|
|
|
33
33
|
"_powerUserExample": {
|
|
34
34
|
"_readme": [
|
|
35
35
|
"A narrower life-sphere set for users who want tighter buckets.",
|
|
36
|
-
"
|
|
36
|
+
"The PRIMARY domain is derived by argmax association weight (LLM tag order breaks ties) — no manual override flag.",
|
|
37
37
|
"`weakPrimaryFloor` (default 0.45) is the minimum embedding similarity for a no-fit memory to earn a weak primary; tune it from your corpus."
|
|
38
38
|
],
|
|
39
39
|
"domains": [
|
|
40
40
|
{
|
|
41
41
|
"name": "Work",
|
|
42
|
-
"description": "Employer, day job, client projects, workstreams"
|
|
43
|
-
"compartment": true
|
|
42
|
+
"description": "Employer, day job, client projects, workstreams"
|
|
44
43
|
},
|
|
45
44
|
{
|
|
46
45
|
"name": "Personal",
|
|
@@ -22,7 +22,7 @@ That's the whole surface. No `sync_turn`, no compaction/session-end capture —
|
|
|
22
22
|
|
|
23
23
|
### Pushed recall index (0.7.0, server ≥ 0.14)
|
|
24
24
|
|
|
25
|
-
Instead of injecting full memory content every turn, `prefetch` sends the user's message to the server's `POST /recall-index` and injects the returned **index block** verbatim — one line per memory (id, title, date), capped and relevance-gated server-side. The agent fetches full content with `hicortex_get(id)` only when a line is actually relevant; that fetch is what strengthens the memory (exposure ≠ use). All tuning knobs (`recallMaxItems`, `recallMinSimilarity`, `recallReshowTurns`, `recallMinPromptChars`, …) live in the **server** config — the plugin carries none. Dedup is turn-based and server-side per session; the plugin resets it at `initialize` (the Hermes `MemoryProvider` interface exposes no compaction signal, so a mid-session context rebuild cannot trigger a reset — the server's turn-based re-show window covers that gap). Against a pre-0.14 server (404) the plugin falls back to the 0.6.x `GET /search` full-content prefetch, fail-soft, re-probing the endpoint every 10 minutes so a later server upgrade is picked up without a gateway restart. The recall calls carry the profile's configured `
|
|
25
|
+
Instead of injecting full memory content every turn, `prefetch` sends the user's message to the server's `POST /recall-index` and injects the returned **index block** verbatim — one line per memory (id, title, date), capped and relevance-gated server-side. The agent fetches full content with `hicortex_get(id)` only when a line is actually relevant; that fetch is what strengthens the memory (exposure ≠ use). All tuning knobs (`recallMaxItems`, `recallMinSimilarity`, `recallReshowTurns`, `recallMinPromptChars`, …) live in the **server** config — the plugin carries none. Dedup is turn-based and server-side per session; the plugin resets it at `initialize` (the Hermes `MemoryProvider` interface exposes no compaction signal, so a mid-session context rebuild cannot trigger a reset — the server's turn-based re-show window covers that gap). Against a pre-0.14 server (404) the plugin falls back to the 0.6.x `GET /search` full-content prefetch, fail-soft, re-probing the endpoint every 10 minutes so a later server upgrade is picked up without a gateway restart. The recall calls carry the profile's configured `default_project` (and `mission_domains`) and use a short dedicated timeout (1.5 s) so a slow server can never stall a turn. (`privacy_filter` is deprecated since 0.7.2 — the server ignores privacy; see [Configuration](#configuration).)
|
|
26
26
|
|
|
27
27
|
### Per-agent standing context (0.13)
|
|
28
28
|
|
|
@@ -84,6 +84,8 @@ export HICORTEX_AUTH_TOKEN=hctx-default-token # or your custom token
|
|
|
84
84
|
|
|
85
85
|
Env overrides: `HICORTEX_URL`, `HICORTEX_AUTH_TOKEN`.
|
|
86
86
|
|
|
87
|
+
> **`privacy_filter` is DEPRECATED** (plugin 0.7.2 / server 0.16.2). The server no longer filters on privacy — the `privacy` column is vestigial (stored, never filtered). The setting is still accepted for backward compatibility but is now a harmless no-op; setting it emits a one-time-per-process warning in the gateway log. For work/personal isolation, run a **separate Hicortex server** per scope rather than relying on in-server privacy filtering.
|
|
88
|
+
|
|
87
89
|
## Topology
|
|
88
90
|
|
|
89
91
|
- **Server host:** runs Hicortex. Set `hicortex_url: http://localhost:8787` (localhost bypasses auth).
|
|
@@ -8,9 +8,15 @@ plugin also works with env-only setup.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import logging
|
|
11
12
|
import os
|
|
12
13
|
from typing import Any, Dict, Optional
|
|
13
14
|
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# One-time-per-process guard for the privacy_filter deprecation warning.
|
|
18
|
+
_privacy_filter_deprecation_warned = False
|
|
19
|
+
|
|
14
20
|
# Declarative config schema — drives `hermes memory setup` (see MemoryProvider
|
|
15
21
|
# .get_config_schema). Field shape per the Hermes MemoryProvider contract:
|
|
16
22
|
# key, label, description, default, required, secret, env_var, choices, url.
|
|
@@ -21,7 +27,7 @@ CONFIG_SCHEMA: list[dict[str, Any]] = [
|
|
|
21
27
|
"description": (
|
|
22
28
|
"URL of the Hicortex memory server. On the server host use "
|
|
23
29
|
"http://localhost:8787; on other machines use the server's "
|
|
24
|
-
"
|
|
30
|
+
"private hostname, e.g. http://memory-server:8787."
|
|
25
31
|
),
|
|
26
32
|
"default": "http://localhost:8787",
|
|
27
33
|
"required": True,
|
|
@@ -56,8 +62,15 @@ CONFIG_SCHEMA: list[dict[str, Any]] = [
|
|
|
56
62
|
},
|
|
57
63
|
{
|
|
58
64
|
"key": "privacy_filter",
|
|
59
|
-
"label": "Privacy filter",
|
|
60
|
-
"description":
|
|
65
|
+
"label": "Privacy filter (DEPRECATED)",
|
|
66
|
+
"description": (
|
|
67
|
+
"DEPRECATED since plugin 0.7.2 / server 0.16.2. The server no "
|
|
68
|
+
"longer filters on privacy — the column is vestigial. This setting "
|
|
69
|
+
"is now a harmless no-op: it is still accepted for backward "
|
|
70
|
+
"compat but ignored. For work/personal isolation, run a separate "
|
|
71
|
+
"Hicortex server per scope. (Historically: comma-separated privacy "
|
|
72
|
+
"levels to include, e.g. WORK,PERSONAL.)"
|
|
73
|
+
),
|
|
61
74
|
"default": "WORK,PERSONAL",
|
|
62
75
|
"required": False,
|
|
63
76
|
},
|
|
@@ -94,14 +107,19 @@ def _config_path(hermes_home: Optional[str] = None) -> str:
|
|
|
94
107
|
|
|
95
108
|
def load_config() -> Dict[str, Any]:
|
|
96
109
|
"""Load merged config: file <- env overrides <- defaults."""
|
|
110
|
+
global _privacy_filter_deprecation_warned
|
|
97
111
|
path = _config_path()
|
|
98
112
|
cfg: Dict[str, Any] = {}
|
|
113
|
+
file_set_privacy_filter = False
|
|
99
114
|
if os.path.exists(path):
|
|
100
115
|
try:
|
|
101
116
|
with open(path, encoding="utf-8") as f:
|
|
102
117
|
cfg = json.load(f) or {}
|
|
103
118
|
except Exception:
|
|
104
119
|
cfg = {}
|
|
120
|
+
# Detect an EXPLICIT user setting (the default is applied via setdefault
|
|
121
|
+
# below); only warn when the profile actually configured it.
|
|
122
|
+
file_set_privacy_filter = "privacy_filter" in cfg
|
|
105
123
|
|
|
106
124
|
# Env overrides
|
|
107
125
|
if os.environ.get("HICORTEX_URL"):
|
|
@@ -113,6 +131,19 @@ def load_config() -> Dict[str, Any]:
|
|
|
113
131
|
cfg.setdefault("hicortex_url", "http://localhost:8787")
|
|
114
132
|
cfg.setdefault("recall_limit", 5)
|
|
115
133
|
cfg.setdefault("privacy_filter", "WORK,PERSONAL")
|
|
134
|
+
|
|
135
|
+
# 0.16.2 deprecation: privacy_filter is a no-op now (server ignores privacy
|
|
136
|
+
# entirely). Warn once per process if the profile explicitly sets it.
|
|
137
|
+
if file_set_privacy_filter and not _privacy_filter_deprecation_warned:
|
|
138
|
+
_privacy_filter_deprecation_warned = True
|
|
139
|
+
logger.warning(
|
|
140
|
+
"hicortex: config.json sets 'privacy_filter', which is deprecated "
|
|
141
|
+
"since plugin 0.7.2 / server 0.16.2 — the server no longer filters "
|
|
142
|
+
"on privacy (the column is vestigial). It is a harmless no-op now. "
|
|
143
|
+
"For work/personal isolation, run a separate Hicortex server per "
|
|
144
|
+
"scope. (This warning fires once per process.)"
|
|
145
|
+
)
|
|
146
|
+
|
|
116
147
|
return cfg
|
|
117
148
|
|
|
118
149
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: hicortex
|
|
2
|
-
version: 0.7.
|
|
2
|
+
version: 0.7.2
|
|
3
3
|
description: "Self-learning memory for Hermes agents — every session is distilled into lessons overnight, and your agent wakes up wiser. Pushes a compact per-turn recall index (lazy-loaded with hicortex_get), injects fresh lessons plus a per-agent standing context block, and exposes the full 9-tool memory surface (search, get, recent, ingest, lessons, index, graph, update, delete) via a shared Hicortex server. Stdlib-only."
|
|
4
4
|
pip_dependencies: []
|
|
5
5
|
hooks: []
|
|
@@ -210,7 +210,8 @@ class HicortexProvider(MemoryProvider):
|
|
|
210
210
|
self._recall_limit = 5
|
|
211
211
|
self._privacy = cfg.get("privacy_filter", "WORK,PERSONAL")
|
|
212
212
|
# #203 scope: declared knowledge domains for this role-bound agent
|
|
213
|
-
# (e.g.
|
|
213
|
+
# (e.g. a health-focused agent → Health). Soft affinity boost on
|
|
214
|
+
# recall; never excludes.
|
|
214
215
|
_md_raw = cfg.get("mission_domains") or ""
|
|
215
216
|
self._mission_domains = [d.strip() for d in _md_raw.split(",") if d.strip()]
|
|
216
217
|
self._agent_name = _resolve_agent_name(cfg)
|
|
@@ -446,6 +447,11 @@ class HicortexProvider(MemoryProvider):
|
|
|
446
447
|
lines.append("Lessons:")
|
|
447
448
|
for l in lessons:
|
|
448
449
|
c = (l.get("content") or "").strip().replace("\n", " ")
|
|
450
|
+
# Legacy lessons were stored with a "## Lesson:" prefix; new ones are
|
|
451
|
+
# topic-first (selected by memory_type, not the prefix). Strip it so
|
|
452
|
+
# Hermes renders the same topic-first line as the CC/OC lessons blocks.
|
|
453
|
+
if c.startswith("## Lesson: "):
|
|
454
|
+
c = c[len("## Lesson: "):]
|
|
449
455
|
lines.append(f"- {c[:200]}")
|
|
450
456
|
if idx.get("total"):
|
|
451
457
|
lines.append(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
4
4
|
"description": "Self-learning memory for AI agents — experience captured automatically, distilled into lessons overnight, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|