@gamaze/hicortex 0.13.2 → 0.14.0
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 +15 -1
- package/dist/cli.js +12 -0
- package/dist/consolidate.d.ts +1 -1
- package/dist/consolidate.js +1 -1
- package/dist/context-cli.js +1 -1
- package/dist/db.js +14 -0
- package/dist/domain-classify.d.ts +3 -3
- package/dist/domain-classify.js +3 -3
- package/dist/hermes-transcript-reader.d.ts +1 -1
- package/dist/hermes-transcript-reader.js +1 -1
- package/dist/init.d.ts +7 -0
- package/dist/init.js +44 -16
- package/dist/lessons-context.d.ts +15 -1
- package/dist/lessons-context.js +5 -2
- package/dist/mcp-server.js +91 -6
- package/dist/nightly.js +5 -0
- package/dist/pi-transcript-reader.d.ts +3 -3
- package/dist/pi-transcript-reader.js +5 -5
- package/dist/recall-hook-cli.d.ts +28 -0
- package/dist/recall-hook-cli.js +77 -0
- package/dist/recall-index.d.ts +54 -0
- package/dist/recall-index.js +163 -0
- package/dist/recall-registry.d.ts +48 -0
- package/dist/recall-registry.js +99 -0
- package/dist/retrieval.d.ts +39 -1
- package/dist/retrieval.js +122 -22
- package/dist/storage.d.ts +8 -1
- package/dist/storage.js +27 -1
- package/dist/transcript-reader.d.ts +1 -1
- package/dist/transcript-reader.js +2 -2
- package/dist/types.d.ts +6 -0
- package/domains.example.json +53 -13
- package/hermes-plugin/hicortex/provider.py +4 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/retrieval.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* score = similarity * 0.4 + effective_strength * 0.3 + connection_score * 0.2 + recency * 0.1
|
|
8
8
|
*
|
|
9
9
|
* Decay model (B+E+D):
|
|
10
|
-
* base_decay =
|
|
10
|
+
* base_decay = derived from decayHalfLifeDays (config; default 365 → ~1-year
|
|
11
|
+
* half-life at importance 0.5, importance-scaled either way)
|
|
11
12
|
* decay_rate = 1 - base_decay * (1 - importance)
|
|
12
13
|
* decay_rate = 1 - (1 - decay_rate) * 0.7^access_count
|
|
13
14
|
* decay_rate = 1 - (1 - decay_rate) * 0.7^link_count
|
|
@@ -48,13 +49,67 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
48
49
|
};
|
|
49
50
|
})();
|
|
50
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.DEFAULT_DECAY_HALF_LIFE_DAYS = void 0;
|
|
53
|
+
exports.decayConstantForHalfLife = decayConstantForHalfLife;
|
|
54
|
+
exports.configureDecay = configureDecay;
|
|
55
|
+
exports.configureRecall = configureRecall;
|
|
51
56
|
exports.l2ToCosine = l2ToCosine;
|
|
52
57
|
exports.effectiveStrength = effectiveStrength;
|
|
53
58
|
exports.computeScore = computeScore;
|
|
54
59
|
exports.retrieve = retrieve;
|
|
55
60
|
exports.searchRecent = searchRecent;
|
|
56
61
|
const storage = __importStar(require("./storage.js"));
|
|
57
|
-
|
|
62
|
+
/** Default decay half-life (days) at importance 0.5. #192: was 0.0005/h
|
|
63
|
+
* (~115-day half-life at base 0.5) — aggressive enough to bury the long tail
|
|
64
|
+
* in ranking. Long-term remembering is the product; time preference stays,
|
|
65
|
+
* but mild. */
|
|
66
|
+
exports.DEFAULT_DECAY_HALF_LIFE_DAYS = 365;
|
|
67
|
+
/**
|
|
68
|
+
* Derive the per-hour base decay constant from a half-life target: for the
|
|
69
|
+
* decayable portion, retention^hours = 0.5 at `days`, evaluated at the
|
|
70
|
+
* reference importance 0.5 (the model scales the rate by (1 − importance)).
|
|
71
|
+
* decay_rate = 1 − λ(1 − imp) ⇒ half-life ≈ ln2 / (λ·(1 − imp)), so
|
|
72
|
+
* λ = ln2 / (24·days·0.5).
|
|
73
|
+
*/
|
|
74
|
+
function decayConstantForHalfLife(days) {
|
|
75
|
+
return Math.LN2 / (24 * days * 0.5);
|
|
76
|
+
}
|
|
77
|
+
let BASE_DECAY = decayConstantForHalfLife(exports.DEFAULT_DECAY_HALF_LIFE_DAYS);
|
|
78
|
+
/**
|
|
79
|
+
* Configure the decay speed from config (`decayHalfLifeDays`). Called at boot
|
|
80
|
+
* by the server and the nightly so both processes score with the same clock.
|
|
81
|
+
* Invalid/absent values keep the default. Exported value for tests.
|
|
82
|
+
*/
|
|
83
|
+
function configureDecay(options) {
|
|
84
|
+
const days = Number(options?.halfLifeDays);
|
|
85
|
+
BASE_DECAY = decayConstantForHalfLife(Number.isFinite(days) && days > 0 ? days : exports.DEFAULT_DECAY_HALF_LIFE_DAYS);
|
|
86
|
+
return BASE_DECAY;
|
|
87
|
+
}
|
|
88
|
+
const RECALL_DEFAULTS = {
|
|
89
|
+
searchLimit: 8,
|
|
90
|
+
recentLimit: 12,
|
|
91
|
+
recentWindowDays: 180,
|
|
92
|
+
coldExposureSlots: 2,
|
|
93
|
+
};
|
|
94
|
+
let recallDefaults = { ...RECALL_DEFAULTS };
|
|
95
|
+
/**
|
|
96
|
+
* Configure recall breadth from config. Called at boot next to
|
|
97
|
+
* configureDecay(); invalid/absent values keep the shipped defaults.
|
|
98
|
+
* Returns the resolved values (for logging + tests).
|
|
99
|
+
*/
|
|
100
|
+
function configureRecall(config) {
|
|
101
|
+
const pick = (key) => {
|
|
102
|
+
const v = Number(config?.[key]);
|
|
103
|
+
return Number.isFinite(v) && v >= 0 ? Math.floor(v) : RECALL_DEFAULTS[key];
|
|
104
|
+
};
|
|
105
|
+
recallDefaults = {
|
|
106
|
+
searchLimit: Math.max(1, pick("searchLimit")),
|
|
107
|
+
recentLimit: Math.max(1, pick("recentLimit")),
|
|
108
|
+
recentWindowDays: Math.max(1, pick("recentWindowDays")),
|
|
109
|
+
coldExposureSlots: pick("coldExposureSlots"),
|
|
110
|
+
};
|
|
111
|
+
return { ...recallDefaults };
|
|
112
|
+
}
|
|
58
113
|
/**
|
|
59
114
|
* Placeholder L2 distance for candidates that have no measured vector
|
|
60
115
|
* distance (FTS-only hits and graph-discovered neighbors). Chosen so that
|
|
@@ -177,7 +232,7 @@ function collectLinks(db, seedIds, maxHops = 2) {
|
|
|
177
232
|
// ---------------------------------------------------------------------------
|
|
178
233
|
// Formatting
|
|
179
234
|
// ---------------------------------------------------------------------------
|
|
180
|
-
function formatResult(memory, score, effStr, connections) {
|
|
235
|
+
function formatResult(memory, score, effStr, connections, provenance) {
|
|
181
236
|
return {
|
|
182
237
|
id: memory.id,
|
|
183
238
|
content: memory.content ?? "",
|
|
@@ -188,6 +243,8 @@ function formatResult(memory, score, effStr, connections) {
|
|
|
188
243
|
project: memory.project ?? null,
|
|
189
244
|
created_at: memory.created_at ?? "",
|
|
190
245
|
connections,
|
|
246
|
+
similarity: provenance ? provenance.similarity : undefined,
|
|
247
|
+
source: provenance ? provenance.source : undefined,
|
|
191
248
|
};
|
|
192
249
|
}
|
|
193
250
|
// ---------------------------------------------------------------------------
|
|
@@ -224,19 +281,24 @@ function reciprocalRankFusion(rankedLists, k = RRF_K) {
|
|
|
224
281
|
* and composite scoring. Strengthens accessed memories.
|
|
225
282
|
*/
|
|
226
283
|
async function retrieve(db, embedFn, query, options) {
|
|
227
|
-
const limit = options?.limit ??
|
|
284
|
+
const limit = options?.limit ?? recallDefaults.searchLimit;
|
|
228
285
|
const project = options?.project;
|
|
229
286
|
const privacy = options?.privacy;
|
|
230
287
|
const sourceAgent = options?.sourceAgent;
|
|
231
288
|
const now = new Date();
|
|
232
289
|
// 1. Embed
|
|
233
290
|
const queryEmbedding = await embedFn(query);
|
|
234
|
-
// 2. Dual retrieval — vector + BM25
|
|
235
|
-
|
|
291
|
+
// 2. Dual retrieval — vector + BM25.
|
|
292
|
+
// #192: sqlite-vec can't push filters into the KNN, so filtered queries must
|
|
293
|
+
// over-fetch — the old flat limit*3 intersected a global top-15 with (for the
|
|
294
|
+
// median project) ~1% of the corpus, starving every filtered query.
|
|
295
|
+
const filtered = Boolean(project || privacy || sourceAgent);
|
|
296
|
+
const fetchLimit = filtered ? Math.min(limit * 20, 200) : limit * 3;
|
|
236
297
|
let vecCandidates = storage.vectorSearch(db, queryEmbedding, fetchLimit, []);
|
|
237
298
|
let ftsCandidates = [];
|
|
238
299
|
try {
|
|
239
|
-
|
|
300
|
+
// privacy/sourceAgent/project are all pushed into the FTS SQL.
|
|
301
|
+
ftsCandidates = storage.searchFts(db, query, fetchLimit, privacy, sourceAgent, project ?? undefined);
|
|
240
302
|
}
|
|
241
303
|
catch {
|
|
242
304
|
// FTS5 search can fail on special characters; fall back to vector-only
|
|
@@ -247,7 +309,6 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
247
309
|
// Post-filter vector candidates (sqlite-vec can't filter)
|
|
248
310
|
if (project) {
|
|
249
311
|
vecCandidates = vecCandidates.filter((c) => c.project === project);
|
|
250
|
-
ftsCandidates = ftsCandidates.filter((c) => c.project === project);
|
|
251
312
|
}
|
|
252
313
|
if (privacy) {
|
|
253
314
|
vecCandidates = vecCandidates.filter((c) => privacy.includes(c.privacy));
|
|
@@ -259,14 +320,18 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
259
320
|
const vecRanked = vecCandidates.map((c) => c.id);
|
|
260
321
|
const ftsRanked = ftsCandidates.map((c) => c.id);
|
|
261
322
|
const rrfScores = reciprocalRankFusion([vecRanked, ftsRanked]);
|
|
262
|
-
// Build unified candidate map
|
|
323
|
+
// Build unified candidate map (with retrieval-channel provenance, #192)
|
|
263
324
|
const candidateMap = new Map();
|
|
264
325
|
for (const c of vecCandidates) {
|
|
265
|
-
candidateMap.set(c.id, { mem: c, distance: c.distance });
|
|
326
|
+
candidateMap.set(c.id, { mem: c, distance: c.distance, source: "vector" });
|
|
266
327
|
}
|
|
267
328
|
for (const c of ftsCandidates) {
|
|
268
|
-
|
|
269
|
-
|
|
329
|
+
const existing = candidateMap.get(c.id);
|
|
330
|
+
if (existing) {
|
|
331
|
+
existing.source = "both";
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
candidateMap.set(c.id, { mem: c, distance: DEFAULT_GRAPH_DISTANCE, source: "fts" });
|
|
270
335
|
}
|
|
271
336
|
}
|
|
272
337
|
// 4. Graph traversal
|
|
@@ -284,7 +349,7 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
284
349
|
continue;
|
|
285
350
|
if (sourceAgent && mem.source_agent !== sourceAgent)
|
|
286
351
|
continue;
|
|
287
|
-
candidateMap.set(gid, { mem, distance: DEFAULT_GRAPH_DISTANCE });
|
|
352
|
+
candidateMap.set(gid, { mem, distance: DEFAULT_GRAPH_DISTANCE, source: "graph" });
|
|
288
353
|
}
|
|
289
354
|
// 5. Compute composite scores
|
|
290
355
|
const maxConnections = Math.max(...([...connectionCounts.values()].length > 0
|
|
@@ -292,7 +357,7 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
292
357
|
: [0]));
|
|
293
358
|
const scored = [];
|
|
294
359
|
const maxRrf = Math.max(...([...rrfScores.values()].length > 0 ? [...rrfScores.values()] : [1]));
|
|
295
|
-
for (const [mid, { mem, distance }] of candidateMap) {
|
|
360
|
+
for (const [mid, { mem, distance, source }] of candidateMap) {
|
|
296
361
|
const connCount = connectionCounts.get(mid) ?? 0;
|
|
297
362
|
const composite = computeScore(mem, distance, connCount, maxConnections, now);
|
|
298
363
|
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now, {
|
|
@@ -302,25 +367,60 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
302
367
|
const rrf = rrfScores.get(mid) ?? 0;
|
|
303
368
|
const normalizedRrf = maxRrf > 0 ? rrf / maxRrf : 0;
|
|
304
369
|
const finalScore = composite * 0.8 + normalizedRrf * 0.2;
|
|
305
|
-
|
|
370
|
+
// Measured cosine only for vector-matched candidates; FTS/graph hits carry
|
|
371
|
+
// the neutral placeholder distance, which is not a real similarity.
|
|
372
|
+
const similarity = source === "vector" || source === "both"
|
|
373
|
+
? Math.round(l2ToCosine(distance) * 1e6) / 1e6
|
|
374
|
+
: null;
|
|
375
|
+
scored.push({ mem, finalScore, effStr, connCount, similarity, source });
|
|
306
376
|
}
|
|
307
|
-
// 6. Sort and take top N
|
|
377
|
+
// 6. Sort and take top N — with cold-exposure slots (#192).
|
|
378
|
+
// Access hardening + effective strength make past winners self-reinforcing:
|
|
379
|
+
// 88% of the production corpus had never been returned by any query. Reserve
|
|
380
|
+
// up to 2 of k for the best-scoring never-accessed candidates so the long
|
|
381
|
+
// tail gets nonzero exposure whenever it is semantically in range. Slots are
|
|
382
|
+
// only "reserved" when cold candidates exist; otherwise the top-k is the
|
|
383
|
+
// plain score order.
|
|
308
384
|
scored.sort((a, b) => b.finalScore - a.finalScore);
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
385
|
+
const coldSlots = limit >= 4 ? recallDefaults.coldExposureSlots : 0;
|
|
386
|
+
let top = scored.slice(0, limit);
|
|
387
|
+
if (coldSlots > 0 && scored.length > limit) {
|
|
388
|
+
const coldInTop = top.filter((t) => (t.mem.access_count ?? 0) === 0).length;
|
|
389
|
+
const wanted = coldSlots - coldInTop;
|
|
390
|
+
if (wanted > 0) {
|
|
391
|
+
const coldExtras = scored
|
|
392
|
+
.slice(limit)
|
|
393
|
+
.filter((t) => (t.mem.access_count ?? 0) === 0)
|
|
394
|
+
.slice(0, wanted);
|
|
395
|
+
if (coldExtras.length > 0) {
|
|
396
|
+
top = [...top.slice(0, limit - coldExtras.length), ...coldExtras];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const results = top.map((t) => formatResult(t.mem, t.finalScore, t.effStr, t.connCount, {
|
|
401
|
+
similarity: t.similarity,
|
|
402
|
+
source: t.source,
|
|
403
|
+
}));
|
|
404
|
+
// 7. Strengthen — skipped for pushed recall (#192): appearing in a pushed
|
|
405
|
+
// index is exposure, not use; the /recall-index path records shown_count +
|
|
406
|
+
// last_accessed via storage.touchMemoriesShown instead.
|
|
407
|
+
if (!options?.noStrengthen) {
|
|
408
|
+
strengthen(db, top.map((t) => t.mem), now);
|
|
409
|
+
}
|
|
313
410
|
return results;
|
|
314
411
|
}
|
|
315
412
|
/**
|
|
316
413
|
* Get recent context, optionally filtered by project and privacy.
|
|
317
414
|
*/
|
|
318
415
|
function searchRecent(db, options) {
|
|
319
|
-
const limit = options?.limit ??
|
|
416
|
+
const limit = options?.limit ?? recallDefaults.recentLimit;
|
|
320
417
|
const project = options?.project;
|
|
321
418
|
const privacy = options?.privacy;
|
|
322
419
|
const now = new Date();
|
|
323
|
-
|
|
420
|
+
// #192 breadth: 30 → 180-day default window (config recentWindowDays).
|
|
421
|
+
// "Recent" for a long-lived corpus is a season, not a month; the narrow
|
|
422
|
+
// window kept queryless recall re-serving the same few weeks.
|
|
423
|
+
let candidates = storage.getRecentMemories(db, recallDefaults.recentWindowDays, limit * 3);
|
|
324
424
|
if (project) {
|
|
325
425
|
candidates = candidates.filter((c) => c.project === project);
|
|
326
426
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -30,6 +30,13 @@ export declare function updateMemory(db: Database.Database, memoryId: string, fi
|
|
|
30
30
|
* Atomically increment access_count and reset last_accessed.
|
|
31
31
|
*/
|
|
32
32
|
export declare function strengthenMemory(db: Database.Database, memoryId: string, nowIsoStr: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Record that memories appeared in a pushed recall index (#192): bump
|
|
35
|
+
* shown_count and refresh last_accessed (a mild strengthen — the decay clock
|
|
36
|
+
* resets so topically-live memories stop sinking) WITHOUT touching
|
|
37
|
+
* access_count, which stays reserved for real use (hardening + prune shield).
|
|
38
|
+
*/
|
|
39
|
+
export declare function touchMemoriesShown(db: Database.Database, memoryIds: string[], nowIsoStr: string): void;
|
|
33
40
|
/**
|
|
34
41
|
* Delete a memory, its vector, its tags, and all its links.
|
|
35
42
|
*/
|
|
@@ -87,7 +94,7 @@ export declare function vectorSearch(db: Database.Database, queryEmbedding: Floa
|
|
|
87
94
|
* Full-text search using FTS5 BM25 ranking.
|
|
88
95
|
* Returns memories with a rank field (lower is better).
|
|
89
96
|
*/
|
|
90
|
-
export declare function searchFts(db: Database.Database, query: string, limit?: number, privacy?: string[], sourceAgent?: string): Array<Memory & {
|
|
97
|
+
export declare function searchFts(db: Database.Database, query: string, limit?: number, privacy?: string[], sourceAgent?: string, project?: string): Array<Memory & {
|
|
91
98
|
rank: number;
|
|
92
99
|
}>;
|
|
93
100
|
/**
|
package/dist/storage.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.insertMemory = insertMemory;
|
|
|
9
9
|
exports.getMemory = getMemory;
|
|
10
10
|
exports.updateMemory = updateMemory;
|
|
11
11
|
exports.strengthenMemory = strengthenMemory;
|
|
12
|
+
exports.touchMemoriesShown = touchMemoriesShown;
|
|
12
13
|
exports.deleteMemory = deleteMemory;
|
|
13
14
|
exports.setMemoryTags = setMemoryTags;
|
|
14
15
|
exports.getMemoryTags = getMemoryTags;
|
|
@@ -130,6 +131,24 @@ function strengthenMemory(db, memoryId, nowIsoStr) {
|
|
|
130
131
|
SET access_count = access_count + 1, last_accessed = ?
|
|
131
132
|
WHERE id = ?`).run(nowIsoStr, memoryId);
|
|
132
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Record that memories appeared in a pushed recall index (#192): bump
|
|
136
|
+
* shown_count and refresh last_accessed (a mild strengthen — the decay clock
|
|
137
|
+
* resets so topically-live memories stop sinking) WITHOUT touching
|
|
138
|
+
* access_count, which stays reserved for real use (hardening + prune shield).
|
|
139
|
+
*/
|
|
140
|
+
function touchMemoriesShown(db, memoryIds, nowIsoStr) {
|
|
141
|
+
if (memoryIds.length === 0)
|
|
142
|
+
return;
|
|
143
|
+
const stmt = db.prepare(`UPDATE memories
|
|
144
|
+
SET shown_count = COALESCE(shown_count, 0) + 1, last_accessed = ?
|
|
145
|
+
WHERE id = ?`);
|
|
146
|
+
const run = db.transaction((ids) => {
|
|
147
|
+
for (const id of ids)
|
|
148
|
+
stmt.run(nowIsoStr, id);
|
|
149
|
+
});
|
|
150
|
+
run(memoryIds);
|
|
151
|
+
}
|
|
133
152
|
/**
|
|
134
153
|
* Delete a memory, its vector, its tags, and all its links.
|
|
135
154
|
*/
|
|
@@ -234,7 +253,7 @@ function vectorSearch(db, queryEmbedding, limit = 10, excludeIds = []) {
|
|
|
234
253
|
* Full-text search using FTS5 BM25 ranking.
|
|
235
254
|
* Returns memories with a rank field (lower is better).
|
|
236
255
|
*/
|
|
237
|
-
function searchFts(db, query, limit = 10, privacy, sourceAgent) {
|
|
256
|
+
function searchFts(db, query, limit = 10, privacy, sourceAgent, project) {
|
|
238
257
|
const conditions = ["memories_fts MATCH ?"];
|
|
239
258
|
const params = [query];
|
|
240
259
|
if (privacy && privacy.length > 0) {
|
|
@@ -246,6 +265,13 @@ function searchFts(db, query, limit = 10, privacy, sourceAgent) {
|
|
|
246
265
|
conditions.push("m.source_agent = ?");
|
|
247
266
|
params.push(sourceAgent);
|
|
248
267
|
}
|
|
268
|
+
// #192: project is pushed into SQL (like privacy/sourceAgent) instead of
|
|
269
|
+
// being post-filtered by the caller — post-filtering a global top-N against
|
|
270
|
+
// a small project starves the result set regardless of corpus content.
|
|
271
|
+
if (project) {
|
|
272
|
+
conditions.push("m.project = ?");
|
|
273
|
+
params.push(project);
|
|
274
|
+
}
|
|
249
275
|
const where = conditions.join(" AND ");
|
|
250
276
|
params.push(limit);
|
|
251
277
|
const rows = db
|
|
@@ -15,7 +15,7 @@ export interface TranscriptBatch {
|
|
|
15
15
|
date: string;
|
|
16
16
|
entries: unknown[];
|
|
17
17
|
/**
|
|
18
|
-
* Optional source-agent label (e.g. "hermes/
|
|
18
|
+
* Optional source-agent label (e.g. "hermes/alice"). When set, the nightly
|
|
19
19
|
* pipeline uses it verbatim for provenance instead of the default
|
|
20
20
|
* `claude-code/<project>`. Lets per-harness readers stamp their own origin.
|
|
21
21
|
*/
|
|
@@ -159,12 +159,12 @@ function parseTranscriptFile(filePath, projectName, cursorKey, startCursor, gene
|
|
|
159
159
|
}
|
|
160
160
|
/**
|
|
161
161
|
* Decode CC project directory name to a human-readable project name.
|
|
162
|
-
* CC uses path-based hashing: "-Users-
|
|
162
|
+
* CC uses path-based hashing: "-Users-alice-Development-Tools-hicortex"
|
|
163
163
|
* becomes "hicortex" (last path component).
|
|
164
164
|
*/
|
|
165
165
|
function decodeProjectDirName(dirName) {
|
|
166
166
|
// CC encodes paths by replacing / with -
|
|
167
|
-
// e.g. "-Users-
|
|
167
|
+
// e.g. "-Users-alice-Development-Tools-hicortex"
|
|
168
168
|
const parts = dirName.split("-").filter(Boolean);
|
|
169
169
|
if (parts.length === 0)
|
|
170
170
|
return dirName;
|
package/dist/types.d.ts
CHANGED
|
@@ -42,6 +42,12 @@ export interface MemorySearchResult {
|
|
|
42
42
|
project: string | null;
|
|
43
43
|
created_at: string;
|
|
44
44
|
connections: number;
|
|
45
|
+
/** True cosine similarity to the query for vector-matched candidates; null
|
|
46
|
+
* for FTS-only and graph-discovered hits (no measured distance). */
|
|
47
|
+
similarity?: number | null;
|
|
48
|
+
/** Which retrieval channel produced the candidate (vector KNN, BM25 FTS,
|
|
49
|
+
* both, or graph traversal). Used by the /recall-index relevance gate. */
|
|
50
|
+
source?: "vector" | "fts" | "both" | "graph";
|
|
45
51
|
}
|
|
46
52
|
/** Report returned by the consolidation pipeline. */
|
|
47
53
|
export interface ConsolidationReport {
|
package/domains.example.json
CHANGED
|
@@ -9,11 +9,26 @@
|
|
|
9
9
|
"Backfill an existing corpus with: hicortex classify-domains"
|
|
10
10
|
],
|
|
11
11
|
"domains": [
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{
|
|
12
|
+
{
|
|
13
|
+
"name": "Work",
|
|
14
|
+
"description": "Your job and professional life — employer, clients, workstreams"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "Personal",
|
|
18
|
+
"description": "Private life — home, hobbies, everyday matters"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "People",
|
|
22
|
+
"description": "Relationships — family, friends, social life, network"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "Health",
|
|
26
|
+
"description": "Fitness, wellbeing, medical"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "Finance",
|
|
30
|
+
"description": "Money — budgeting, spending, investing"
|
|
31
|
+
}
|
|
17
32
|
],
|
|
18
33
|
"_powerUserExample": {
|
|
19
34
|
"_readme": [
|
|
@@ -22,14 +37,39 @@
|
|
|
22
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."
|
|
23
38
|
],
|
|
24
39
|
"domains": [
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
{
|
|
41
|
+
"name": "Work",
|
|
42
|
+
"description": "Employer, day job, client projects, workstreams",
|
|
43
|
+
"compartment": true
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "Personal",
|
|
47
|
+
"description": "Private life — home, hobbies, everyday matters"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "People",
|
|
51
|
+
"description": "Relationships — family, friends, social life, network"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "Health",
|
|
55
|
+
"description": "Fitness, wellbeing, medical"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "Finance",
|
|
59
|
+
"description": "Money — budgeting, spending, investing"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "Photography",
|
|
63
|
+
"description": "Camera gear, shoots, editing workflow, photo projects"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "Home",
|
|
67
|
+
"description": "House, renovation projects, maintenance, garden"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "Travel",
|
|
71
|
+
"description": "Trips, destinations, bookings, travel plans"
|
|
72
|
+
}
|
|
33
73
|
],
|
|
34
74
|
"weakPrimaryFloor": 0.5
|
|
35
75
|
}
|
|
@@ -33,8 +33,8 @@ _INJECT_CONTENT_CAP = 500
|
|
|
33
33
|
|
|
34
34
|
# Agent ids are joined into a filesystem path server-side, so they share the
|
|
35
35
|
# section-name allowlist. \Z (NOT $) anchors the END OF STRING: Python's $ also
|
|
36
|
-
# matches just before a trailing "\n", so "
|
|
37
|
-
# agent=
|
|
36
|
+
# matches just before a trailing "\n", so "alice\n" would pass and go out as
|
|
37
|
+
# agent=alice%0A → a 400 the fail-soft path silently swallows.
|
|
38
38
|
_AGENT_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*\Z")
|
|
39
39
|
|
|
40
40
|
|
|
@@ -48,7 +48,7 @@ def _sanitize_agent_id(raw: Optional[str]) -> Optional[str]:
|
|
|
48
48
|
EXACTLY so a profile resolves to the SAME id on both harnesses (a mismatch
|
|
49
49
|
would make one honor the persona firewall and the other leak global context
|
|
50
50
|
into an ``off``/``override`` persona): lowercase → collapse invalid runs to
|
|
51
|
-
"-" → strip leading -/_ → truncate 64 → validate. "
|
|
51
|
+
"-" → strip leading -/_ → truncate 64 → validate. "Alice" → "alice";
|
|
52
52
|
"MacBook-Pro.local" → "macbook-pro-local"; all-symbols → None."""
|
|
53
53
|
if not isinstance(raw, str):
|
|
54
54
|
return None
|
|
@@ -73,7 +73,7 @@ def _resolve_agent_name(cfg: Dict[str, Any]) -> Optional[str]:
|
|
|
73
73
|
2. ``HERMES_PROFILE`` env;
|
|
74
74
|
3. parse ``HERMES_HOME`` when it ends ``profiles/<name>``;
|
|
75
75
|
4. None → bare fetch → the global set.
|
|
76
|
-
Each source is stripped then SANITIZED (not rejected) so "
|
|
76
|
+
Each source is stripped then SANITIZED (not rejected) so "Alice" → "alice"
|
|
77
77
|
matches the TS contract; a source that sanitizes to None yields None (bare
|
|
78
78
|
fetch), never a fall-through to another identity."""
|
|
79
79
|
configured = (cfg.get("agent_name") or "").strip()
|
package/openclaw.plugin.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"serverUrl": {
|
|
12
12
|
"type": "string",
|
|
13
13
|
"default": "http://127.0.0.1:8787",
|
|
14
|
-
"description": "Hicortex server URL. Defaults to localhost (co-located server). For multi-machine setups, point this at the remote server (e.g. http://
|
|
14
|
+
"description": "Hicortex server URL. Defaults to localhost (co-located server). For multi-machine setups, point this at the remote server (e.g. http://your-server:8787 or an HTTPS URL)."
|
|
15
15
|
},
|
|
16
16
|
"authToken": {
|
|
17
17
|
"type": "string",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Self-learning memory for AI agents \u2014 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": {
|