@gamaze/hicortex 0.15.1 → 0.15.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 +10 -2
- package/dist/consolidate.d.ts +0 -3
- package/dist/consolidate.js +7 -6
- package/dist/eval/recall-sweep.d.ts +59 -0
- package/dist/eval/recall-sweep.js +715 -0
- package/dist/index.js +1 -1
- package/dist/init.js +27 -0
- package/dist/mcp-server.js +32 -8
- package/dist/memory-instructions.js +1 -1
- package/dist/nightly.js +3 -1
- package/dist/recall-index.d.ts +4 -1
- package/dist/recall-index.js +2 -2
- package/dist/recall-registry.d.ts +39 -1
- package/dist/recall-registry.js +52 -1
- package/dist/retrieval.d.ts +68 -3
- package/dist/retrieval.js +158 -9
- package/dist/schema-prototypes.d.ts +15 -0
- package/dist/schema-prototypes.js +24 -0
- package/dist/storage.js +10 -0
- package/dist/telemetry.d.ts +17 -0
- package/dist/telemetry.js +31 -0
- package/dist/uninstall.js +10 -0
- package/package.json +4 -3
package/dist/retrieval.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
* Retrieval layer with composite scoring, RRF fusion, and graph traversal.
|
|
4
4
|
* Ported from hicortex/retrieval.py — same scoring model and weights.
|
|
5
5
|
*
|
|
6
|
-
* Scoring model:
|
|
7
|
-
* score = similarity * 0.
|
|
6
|
+
* Scoring model (weights are config-driven since 0.15.2 — see configureScoring):
|
|
7
|
+
* score = similarity * 0.50 + effective_strength * 0.20
|
|
8
|
+
* + connection_score * 0.15 + recency * 0.15
|
|
9
|
+
* + fresh-memory bonus (≤ 0.15, linear over the first 7 days)
|
|
10
|
+
* then × 0.50 if the memory was superseded by a later decision
|
|
8
11
|
*
|
|
9
12
|
* Decay model (B+E+D):
|
|
10
13
|
* base_decay = derived from decayHalfLifeDays (config; default 365 → ~1-year
|
|
@@ -49,16 +52,23 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
49
52
|
};
|
|
50
53
|
})();
|
|
51
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
-
exports.DEFAULT_DECAY_HALF_LIFE_DAYS = void 0;
|
|
55
|
+
exports.SESSION_INTENT_ALPHA = exports.DEFAULT_DECAY_HALF_LIFE_DAYS = void 0;
|
|
53
56
|
exports.decayConstantForHalfLife = decayConstantForHalfLife;
|
|
54
57
|
exports.configureDecay = configureDecay;
|
|
55
58
|
exports.configureRecall = configureRecall;
|
|
59
|
+
exports.configureScoring = configureScoring;
|
|
60
|
+
exports.getScoringWeights = getScoringWeights;
|
|
61
|
+
exports.configureSessionIntent = configureSessionIntent;
|
|
62
|
+
exports.getSessionIntent = getSessionIntent;
|
|
63
|
+
exports.blendQueryVector = blendQueryVector;
|
|
64
|
+
exports.findSupersededIds = findSupersededIds;
|
|
56
65
|
exports.l2ToCosine = l2ToCosine;
|
|
57
66
|
exports.effectiveStrength = effectiveStrength;
|
|
58
67
|
exports.computeScore = computeScore;
|
|
59
68
|
exports.retrieve = retrieve;
|
|
60
69
|
exports.searchRecent = searchRecent;
|
|
61
70
|
const storage = __importStar(require("./storage.js"));
|
|
71
|
+
const schema_prototypes_js_1 = require("./schema-prototypes.js");
|
|
62
72
|
/** Default decay half-life (days) at importance 0.5. #192: was 0.0005/h
|
|
63
73
|
* (~115-day half-life at base 0.5) — aggressive enough to bury the long tail
|
|
64
74
|
* in ranking. Long-term remembering is the product; time preference stays,
|
|
@@ -110,6 +120,110 @@ function configureRecall(config) {
|
|
|
110
120
|
};
|
|
111
121
|
return { ...recallDefaults };
|
|
112
122
|
}
|
|
123
|
+
const SCORING_DEFAULTS = {
|
|
124
|
+
similarity: 0.5,
|
|
125
|
+
strength: 0.2,
|
|
126
|
+
connections: 0.15,
|
|
127
|
+
recency: 0.15,
|
|
128
|
+
freshnessBoostDays: 7,
|
|
129
|
+
freshnessBoostWeight: 0.15,
|
|
130
|
+
supersededDemotion: 0.5,
|
|
131
|
+
};
|
|
132
|
+
let scoringWeights = { ...SCORING_DEFAULTS };
|
|
133
|
+
/**
|
|
134
|
+
* Configure scoring weights + ranking knobs from config. Called at boot by the
|
|
135
|
+
* server and the nightly (alongside configureDecay/configureRecall) so
|
|
136
|
+
* retrieval and consolidation rank identically. Invalid/absent values keep the
|
|
137
|
+
* shipped default per key. Returns the resolved set for logging/tests.
|
|
138
|
+
*/
|
|
139
|
+
function configureScoring(config) {
|
|
140
|
+
const num = (key, dflt, min, max) => {
|
|
141
|
+
const v = Number(config?.[key]);
|
|
142
|
+
return Number.isFinite(v) && v >= min && v <= max ? v : dflt;
|
|
143
|
+
};
|
|
144
|
+
scoringWeights = {
|
|
145
|
+
similarity: num("scoreSimilarityWeight", SCORING_DEFAULTS.similarity, 0, 1),
|
|
146
|
+
strength: num("scoreStrengthWeight", SCORING_DEFAULTS.strength, 0, 1),
|
|
147
|
+
connections: num("scoreConnectionsWeight", SCORING_DEFAULTS.connections, 0, 1),
|
|
148
|
+
recency: num("scoreRecencyWeight", SCORING_DEFAULTS.recency, 0, 1),
|
|
149
|
+
freshnessBoostDays: num("freshnessBoostDays", SCORING_DEFAULTS.freshnessBoostDays, 0, 365),
|
|
150
|
+
freshnessBoostWeight: num("freshnessBoostWeight", SCORING_DEFAULTS.freshnessBoostWeight, 0, 1),
|
|
151
|
+
supersededDemotion: num("supersededDemotion", SCORING_DEFAULTS.supersededDemotion, 0, 1),
|
|
152
|
+
};
|
|
153
|
+
return { ...scoringWeights };
|
|
154
|
+
}
|
|
155
|
+
/** Current resolved weights (tests + status output). */
|
|
156
|
+
function getScoringWeights() {
|
|
157
|
+
return { ...scoringWeights };
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Session-intent keying (#192, 0.15.3). ONE config knob:
|
|
161
|
+
// sessionIntentWeight 0.33 blend weight of the rolling centroid in the
|
|
162
|
+
// search vector: query = (1-w)·prompt + w·centroid.
|
|
163
|
+
// 0 = DISABLED (pure prompt, the kill-switch —
|
|
164
|
+
// current behavior). Range [0, 1].
|
|
165
|
+
//
|
|
166
|
+
// The EMA rate α is a shipped constant (SESSION_INTENT_ALPHA, 0.4), not a
|
|
167
|
+
// second knob — owner directive 0.15.3: one knob is enough to tune/disable;
|
|
168
|
+
// exposing α was speculative generality.
|
|
169
|
+
//
|
|
170
|
+
// The centroid itself lives on SessionRecallRegistry; retrieval only needs to
|
|
171
|
+
// ACCEPT a pre-blended query vector (options.queryEmbedding) so the recall
|
|
172
|
+
// closure can do the one-embed-per-recall + blend without retrieve()
|
|
173
|
+
// re-embedding. /search and other unblended callers omit queryEmbedding and
|
|
174
|
+
// get pure-prompt behavior unchanged.
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
/** EMA rate for the session-intent centroid: centroid_new = (1-α)·old + α·prompt. */
|
|
177
|
+
exports.SESSION_INTENT_ALPHA = 0.4;
|
|
178
|
+
const SESSION_INTENT_DEFAULT_WEIGHT = 0.33;
|
|
179
|
+
let sessionIntentWeight = SESSION_INTENT_DEFAULT_WEIGHT;
|
|
180
|
+
/**
|
|
181
|
+
* Configure session-intent keying from config. Called at server boot next to
|
|
182
|
+
* configureScoring (the nightly does no recall, so it does not need this).
|
|
183
|
+
* Reads only `sessionIntentWeight` ([0,1]; 0 = disabled). Invalid/out-of-range
|
|
184
|
+
* values keep the shipped default. Returns `{ weight, alpha }` — alpha is the
|
|
185
|
+
* fixed constant, surfaced so the recall closure passes it to the registry in
|
|
186
|
+
* one call.
|
|
187
|
+
*/
|
|
188
|
+
function configureSessionIntent(config) {
|
|
189
|
+
const v = Number(config?.sessionIntentWeight);
|
|
190
|
+
sessionIntentWeight =
|
|
191
|
+
Number.isFinite(v) && v >= 0 && v <= 1 ? v : SESSION_INTENT_DEFAULT_WEIGHT;
|
|
192
|
+
return { weight: sessionIntentWeight, alpha: exports.SESSION_INTENT_ALPHA };
|
|
193
|
+
}
|
|
194
|
+
/** Current resolved session-intent weight + the shipped alpha (closure + tests). */
|
|
195
|
+
function getSessionIntent() {
|
|
196
|
+
return { weight: sessionIntentWeight, alpha: exports.SESSION_INTENT_ALPHA };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Blend the prompt embedding with the session-intent centroid for the vector
|
|
200
|
+
* search: `query = l2Normalize((1-w)·prompt + w·centroid)`. Returns the prompt
|
|
201
|
+
* UNCHANGED when `centroid` is undefined (first turn — no behavior change) or
|
|
202
|
+
* `weight` is 0 (the kill-switch — pure prompt). Extracted from the
|
|
203
|
+
* /recall-index closure (mcp-server.ts) so the exact blend decision is
|
|
204
|
+
* unit-testable directly, locking the ternary against a refactor without a
|
|
205
|
+
* closure-integration harness.
|
|
206
|
+
*/
|
|
207
|
+
function blendQueryVector(promptEmb, centroid, weight) {
|
|
208
|
+
return centroid && weight > 0
|
|
209
|
+
? (0, schema_prototypes_js_1.l2Normalize)((0, schema_prototypes_js_1.weightedAdd)(promptEmb, 1 - weight, centroid, weight))
|
|
210
|
+
: promptEmb;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Ids among `candidateIds` that have been superseded by a later memory — i.e.
|
|
214
|
+
* they are the SOURCE of a `superseded_by` link (stageSupersession links
|
|
215
|
+
* old → new). One query, not per-candidate.
|
|
216
|
+
*/
|
|
217
|
+
function findSupersededIds(db, candidateIds) {
|
|
218
|
+
if (candidateIds.length === 0)
|
|
219
|
+
return new Set();
|
|
220
|
+
const placeholders = candidateIds.map(() => "?").join(",");
|
|
221
|
+
const rows = db
|
|
222
|
+
.prepare(`SELECT DISTINCT source_id FROM memory_links
|
|
223
|
+
WHERE relationship = 'superseded_by' AND source_id IN (${placeholders})`)
|
|
224
|
+
.all(...candidateIds);
|
|
225
|
+
return new Set(rows.map((r) => r.source_id));
|
|
226
|
+
}
|
|
113
227
|
/**
|
|
114
228
|
* Placeholder L2 distance for candidates that have no measured vector
|
|
115
229
|
* distance (FTS-only hits and graph-discovered neighbors). Chosen so that
|
|
@@ -175,7 +289,7 @@ function effectiveStrength(baseStrength, lastAccessed, now, options) {
|
|
|
175
289
|
* Return a composite relevance score in [0, 1] for a candidate memory.
|
|
176
290
|
* Exported for exact-value tests of the similarity component (#145).
|
|
177
291
|
*/
|
|
178
|
-
function computeScore(memory, distance, connectionCount, maxConnections, now) {
|
|
292
|
+
function computeScore(memory, distance, connectionCount, maxConnections, now, options) {
|
|
179
293
|
// TRUE cosine similarity (#145). The old `1 − distance` compressed real
|
|
180
294
|
// cosines (cos 0.8 scored 0.37) and the 0-clamp at that scale flattened
|
|
181
295
|
// everything below cos 0.5 to exactly 0, killing mid-relevance
|
|
@@ -193,7 +307,34 @@ function computeScore(memory, distance, connectionCount, maxConnections, now) {
|
|
|
193
307
|
const connScore = maxConnections > 0 ? connectionCount / maxConnections : 0;
|
|
194
308
|
const hoursSinceCreated = Math.max((now.getTime() - parseTimestamp(memory.created_at).getTime()) / 3_600_000, 0);
|
|
195
309
|
const recency = Math.pow(0.9995, hoursSinceCreated);
|
|
196
|
-
|
|
310
|
+
const w = scoringWeights;
|
|
311
|
+
let score = similarity * w.similarity +
|
|
312
|
+
effStrength * w.strength +
|
|
313
|
+
connScore * w.connections +
|
|
314
|
+
recency * w.recency;
|
|
315
|
+
// Fresh-memory window (#191 Phase B): a memory is born highly available and
|
|
316
|
+
// settles into the normal ranking over `freshnessBoostDays`. Age is measured
|
|
317
|
+
// from created_at, which the nightly sets from the session's own date — so a
|
|
318
|
+
// session captured last night ranks as ~1 day old (not 0), and backfilled
|
|
319
|
+
// older content correctly gets no boost. The slow
|
|
320
|
+
// `recency` term above (≈58-day half-life at weight 0.15) could never lift a
|
|
321
|
+
// day-old memory past a hardened old one — measured case: an exact-match
|
|
322
|
+
// 1-day-old memory (strength 0.50) lost to an unrelated memory at strength
|
|
323
|
+
// 0.80. This is an ADDITIVE bonus that decays linearly to zero at the window
|
|
324
|
+
// edge, so it cannot distort ranking among memories that are all old.
|
|
325
|
+
const ageDays = hoursSinceCreated / 24;
|
|
326
|
+
if (memory.created_at && ageDays < scoringWeights.freshnessBoostDays) {
|
|
327
|
+
const freshness = 1 - ageDays / scoringWeights.freshnessBoostDays;
|
|
328
|
+
score += freshness * scoringWeights.freshnessBoostWeight;
|
|
329
|
+
}
|
|
330
|
+
// Superseded demotion (#191 Phase B): a memory whose decision was reversed by
|
|
331
|
+
// a later one keeps its content and strength but must not outrank the
|
|
332
|
+
// decision that replaced it. Applied as an explicit multiplier here rather
|
|
333
|
+
// than by penalizing base_strength, so ranking weights stay independently
|
|
334
|
+
// tunable and supersession never nudges a memory toward prune eligibility.
|
|
335
|
+
if (options?.superseded)
|
|
336
|
+
score *= scoringWeights.supersededDemotion;
|
|
337
|
+
return Math.max(0, Math.min(1, score));
|
|
197
338
|
}
|
|
198
339
|
// ---------------------------------------------------------------------------
|
|
199
340
|
// Graph traversal
|
|
@@ -286,8 +427,8 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
286
427
|
const privacy = options?.privacy;
|
|
287
428
|
const sourceAgent = options?.sourceAgent;
|
|
288
429
|
const now = new Date();
|
|
289
|
-
// 1. Embed
|
|
290
|
-
const queryEmbedding = await embedFn(query);
|
|
430
|
+
// 1. Embed — or reuse the caller-provided vector (session-intent blend).
|
|
431
|
+
const queryEmbedding = options?.queryEmbedding ?? (await embedFn(query));
|
|
291
432
|
// 2. Dual retrieval — vector + BM25.
|
|
292
433
|
// #192: sqlite-vec can't push filters into the KNN, so filtered queries must
|
|
293
434
|
// over-fetch — the old flat limit*3 intersected a global top-15 with (for the
|
|
@@ -357,9 +498,14 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
357
498
|
: [0]));
|
|
358
499
|
const scored = [];
|
|
359
500
|
const maxRrf = Math.max(...([...rrfScores.values()].length > 0 ? [...rrfScores.values()] : [1]));
|
|
501
|
+
// One query for the whole candidate set (#191 Phase B): superseded memories
|
|
502
|
+
// are demoted in computeScore rather than strength-penalized.
|
|
503
|
+
const supersededIds = findSupersededIds(db, [...candidateMap.keys()]);
|
|
360
504
|
for (const [mid, { mem, distance, source }] of candidateMap) {
|
|
361
505
|
const connCount = connectionCounts.get(mid) ?? 0;
|
|
362
|
-
const composite = computeScore(mem, distance, connCount, maxConnections, now
|
|
506
|
+
const composite = computeScore(mem, distance, connCount, maxConnections, now, {
|
|
507
|
+
superseded: supersededIds.has(mid),
|
|
508
|
+
});
|
|
363
509
|
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now, {
|
|
364
510
|
accessCount: mem.access_count ?? 0,
|
|
365
511
|
linkCount: connectionCounts.get(mem.id) ?? 0,
|
|
@@ -435,9 +581,12 @@ function searchRecent(db, options) {
|
|
|
435
581
|
? [...connectionCounts.values()]
|
|
436
582
|
: [0]));
|
|
437
583
|
const scored = [];
|
|
584
|
+
const supersededRecent = findSupersededIds(db, candidates.map((c) => c.id));
|
|
438
585
|
for (const mem of candidates) {
|
|
439
586
|
const connCount = connectionCounts.get(mem.id) ?? 0;
|
|
440
|
-
const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, maxConnections, now
|
|
587
|
+
const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, maxConnections, now, {
|
|
588
|
+
superseded: supersededRecent.has(mem.id),
|
|
589
|
+
});
|
|
441
590
|
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now, {
|
|
442
591
|
accessCount: mem.access_count ?? 0,
|
|
443
592
|
linkCount: connectionCounts.get(mem.id) ?? 0,
|
|
@@ -38,6 +38,21 @@ export declare function blobToVec(buf: Buffer): Float32Array;
|
|
|
38
38
|
* is returned as an all-zero copy rather than dividing by zero.
|
|
39
39
|
*/
|
|
40
40
|
export declare function l2Normalize(vec: Float32Array): Float32Array;
|
|
41
|
+
/**
|
|
42
|
+
* Weighted sum of two vectors into a NEW Float32Array (inputs untouched). The
|
|
43
|
+
* result is NOT renormalized — callers normalize explicitly via `l2Normalize`
|
|
44
|
+
* when they need a unit vector (both call sites below do, because embeddings
|
|
45
|
+
* are L2-normalized and the blend must stay on the unit sphere to keep cosine
|
|
46
|
+
* meaningful). Throws on a dimension mismatch rather than silently truncating:
|
|
47
|
+
* the embedding dim is fixed at 384 in practice, so a mismatch signals a
|
|
48
|
+
* mid-process model swap or a bug, which must surface (CLAUDE.md: fail
|
|
49
|
+
* explicitly), not get quietly papered over.
|
|
50
|
+
*
|
|
51
|
+
* Used by the session-intent centroid EMA and the recall query blend (#192
|
|
52
|
+
* session-intent keying): `weightedAdd(a, 1-α, b, α)` is the EMA step,
|
|
53
|
+
* `weightedAdd(prompt, 1-w, centroid, w)` is the blended search vector.
|
|
54
|
+
*/
|
|
55
|
+
export declare function weightedAdd(a: Float32Array, wA: number, b: Float32Array, wB: number): Float32Array;
|
|
41
56
|
/**
|
|
42
57
|
* Association weight of a memory for a tag = cosine(memory embedding, domain
|
|
43
58
|
* prototype). Both inputs are L2-normalized (embedder.ts normalizes memory
|
|
@@ -28,6 +28,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
28
28
|
exports.PROTOTYPE_MIN_MEMBERS = void 0;
|
|
29
29
|
exports.blobToVec = blobToVec;
|
|
30
30
|
exports.l2Normalize = l2Normalize;
|
|
31
|
+
exports.weightedAdd = weightedAdd;
|
|
31
32
|
exports.tagWeight = tagWeight;
|
|
32
33
|
exports.compartmentSet = compartmentSet;
|
|
33
34
|
exports.derivePrimary = derivePrimary;
|
|
@@ -70,6 +71,29 @@ function l2Normalize(vec) {
|
|
|
70
71
|
out[i] = vec[i] / norm;
|
|
71
72
|
return out;
|
|
72
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Weighted sum of two vectors into a NEW Float32Array (inputs untouched). The
|
|
76
|
+
* result is NOT renormalized — callers normalize explicitly via `l2Normalize`
|
|
77
|
+
* when they need a unit vector (both call sites below do, because embeddings
|
|
78
|
+
* are L2-normalized and the blend must stay on the unit sphere to keep cosine
|
|
79
|
+
* meaningful). Throws on a dimension mismatch rather than silently truncating:
|
|
80
|
+
* the embedding dim is fixed at 384 in practice, so a mismatch signals a
|
|
81
|
+
* mid-process model swap or a bug, which must surface (CLAUDE.md: fail
|
|
82
|
+
* explicitly), not get quietly papered over.
|
|
83
|
+
*
|
|
84
|
+
* Used by the session-intent centroid EMA and the recall query blend (#192
|
|
85
|
+
* session-intent keying): `weightedAdd(a, 1-α, b, α)` is the EMA step,
|
|
86
|
+
* `weightedAdd(prompt, 1-w, centroid, w)` is the blended search vector.
|
|
87
|
+
*/
|
|
88
|
+
function weightedAdd(a, wA, b, wB) {
|
|
89
|
+
if (a.length !== b.length) {
|
|
90
|
+
throw new Error(`weightedAdd: dimension mismatch (${a.length} vs ${b.length}) — expected equal-length L2-normalized embeddings`);
|
|
91
|
+
}
|
|
92
|
+
const out = new Float32Array(a.length);
|
|
93
|
+
for (let i = 0; i < a.length; i++)
|
|
94
|
+
out[i] = a[i] * wA + b[i] * wB;
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
73
97
|
/**
|
|
74
98
|
* Association weight of a memory for a tag = cosine(memory embedding, domain
|
|
75
99
|
* prototype). Both inputs are L2-normalized (embedder.ts normalizes memory
|
package/dist/storage.js
CHANGED
|
@@ -335,6 +335,16 @@ function searchFts(db, query, limit = 10, privacy, sourceAgent, project) {
|
|
|
335
335
|
* Create a link between two memories.
|
|
336
336
|
*/
|
|
337
337
|
function addLink(db, sourceId, targetId, relationship, strength = 0.5) {
|
|
338
|
+
// Guard: superseded_by is the sole ranking-demotion signal, so never let a
|
|
339
|
+
// different relationship clobber an existing superseded_by link for the same
|
|
340
|
+
// pair — INSERT OR REPLACE would otherwise silently remove the demotion.
|
|
341
|
+
if (relationship !== "superseded_by") {
|
|
342
|
+
const protectedLink = db
|
|
343
|
+
.prepare("SELECT 1 FROM memory_links WHERE source_id = ? AND target_id = ? AND relationship = 'superseded_by' LIMIT 1")
|
|
344
|
+
.get(sourceId, targetId);
|
|
345
|
+
if (protectedLink)
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
338
348
|
db.prepare(`INSERT OR REPLACE INTO memory_links
|
|
339
349
|
(source_id, target_id, relationship, strength, created_at)
|
|
340
350
|
VALUES (?, ?, ?, ?, ?)`).run(sourceId, targetId, relationship, strength, nowIso());
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* shown — sum of shown_count (server mode only)
|
|
15
15
|
* uses — sum of access_count (server mode only)
|
|
16
16
|
* cold — memories never shown and never used (server mode only)
|
|
17
|
+
* event — install | nightly | uninstall (which lifecycle moment this is)
|
|
17
18
|
*
|
|
18
19
|
* Every install sends the SAME fields — nothing marks an install as special
|
|
19
20
|
* (a rare label would be a fingerprint, i.e. no longer anonymous). Excluding
|
|
@@ -47,6 +48,13 @@ export interface TelemetryPayload {
|
|
|
47
48
|
ok: boolean;
|
|
48
49
|
/** Payload schema version (2 = adoption fields, 0.15.1). Absent = v1. */
|
|
49
50
|
pv?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Lifecycle event this ping represents (0.15.2). Absent on pre-0.15.2
|
|
53
|
+
* payloads, which were always nightlies. Active-install counts are derived
|
|
54
|
+
* from `nightly` events only, so an install/uninstall ping can never look
|
|
55
|
+
* like activity.
|
|
56
|
+
*/
|
|
57
|
+
event?: "install" | "nightly" | "uninstall";
|
|
50
58
|
/**
|
|
51
59
|
* Adoption aggregates (server mode only — a client install has no DB).
|
|
52
60
|
* `shown`/`uses` are corpus-wide sums of shown_count/access_count; their
|
|
@@ -80,3 +88,12 @@ export declare function telemetryDisabledReason(config: Record<string, unknown>
|
|
|
80
88
|
* Send anonymous telemetry. Fire-and-forget — failures are silently ignored.
|
|
81
89
|
*/
|
|
82
90
|
export declare function sendTelemetry(payload: TelemetryPayload, serverUrl?: string): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Send an install/uninstall lifecycle ping (0.15.2). Same anonymous id and
|
|
93
|
+
* transport as the nightly ping, minus the corpus aggregates (there is nothing
|
|
94
|
+
* meaningful to count at install time, and at uninstall the numbers are about
|
|
95
|
+
* to be irrelevant). Fire-and-forget and opt-out-aware like every other ping;
|
|
96
|
+
* exists so the funnel install → first nightly → retained → uninstall is
|
|
97
|
+
* measurable instead of inferred.
|
|
98
|
+
*/
|
|
99
|
+
export declare function sendLifecycleEvent(event: "install" | "uninstall", stateDir: string, config: Record<string, unknown> | null, version: string, serverUrl?: string): Promise<void>;
|
package/dist/telemetry.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* shown — sum of shown_count (server mode only)
|
|
16
16
|
* uses — sum of access_count (server mode only)
|
|
17
17
|
* cold — memories never shown and never used (server mode only)
|
|
18
|
+
* event — install | nightly | uninstall (which lifecycle moment this is)
|
|
18
19
|
*
|
|
19
20
|
* Every install sends the SAME fields — nothing marks an install as special
|
|
20
21
|
* (a rare label would be a fingerprint, i.e. no longer anonymous). Excluding
|
|
@@ -42,6 +43,7 @@ exports.isTelemetryEnabled = isTelemetryEnabled;
|
|
|
42
43
|
exports.getTelemetryId = getTelemetryId;
|
|
43
44
|
exports.telemetryDisabledReason = telemetryDisabledReason;
|
|
44
45
|
exports.sendTelemetry = sendTelemetry;
|
|
46
|
+
exports.sendLifecycleEvent = sendLifecycleEvent;
|
|
45
47
|
const node_crypto_1 = require("node:crypto");
|
|
46
48
|
const state_js_1 = require("./state.js");
|
|
47
49
|
exports.TELEMETRY_URL = "https://hicortex.gamaze.com/api/telemetry";
|
|
@@ -105,3 +107,32 @@ async function sendTelemetry(payload, serverUrl = exports.TELEMETRY_URL) {
|
|
|
105
107
|
// Silently ignore — telemetry must never affect the nightly result
|
|
106
108
|
}
|
|
107
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Send an install/uninstall lifecycle ping (0.15.2). Same anonymous id and
|
|
112
|
+
* transport as the nightly ping, minus the corpus aggregates (there is nothing
|
|
113
|
+
* meaningful to count at install time, and at uninstall the numbers are about
|
|
114
|
+
* to be irrelevant). Fire-and-forget and opt-out-aware like every other ping;
|
|
115
|
+
* exists so the funnel install → first nightly → retained → uninstall is
|
|
116
|
+
* measurable instead of inferred.
|
|
117
|
+
*/
|
|
118
|
+
async function sendLifecycleEvent(event, stateDir, config, version, serverUrl) {
|
|
119
|
+
if (!isTelemetryEnabled(config))
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
await sendTelemetry({
|
|
123
|
+
id: getTelemetryId(stateDir),
|
|
124
|
+
v: version,
|
|
125
|
+
pv: exports.TELEMETRY_PAYLOAD_VERSION,
|
|
126
|
+
event,
|
|
127
|
+
mode: config?.mode === "client" ? "client" : "server",
|
|
128
|
+
agent: "unknown",
|
|
129
|
+
mem: 0,
|
|
130
|
+
lessons: 0,
|
|
131
|
+
sessions: 0,
|
|
132
|
+
ok: true,
|
|
133
|
+
}, serverUrl);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Never let a lifecycle ping affect install/uninstall success.
|
|
137
|
+
}
|
|
138
|
+
}
|
package/dist/uninstall.js
CHANGED
|
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.runUninstall = runUninstall;
|
|
8
8
|
const paths_js_1 = require("./paths.js");
|
|
9
9
|
const node_fs_1 = require("node:fs");
|
|
10
|
+
const telemetry_js_1 = require("./telemetry.js");
|
|
10
11
|
const node_path_1 = require("node:path");
|
|
11
12
|
const node_os_1 = require("node:os");
|
|
12
13
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -26,6 +27,15 @@ async function ask(question) {
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
async function runUninstall() {
|
|
30
|
+
// Churn signal (0.15.2): ping BEFORE removing anything, while state.json
|
|
31
|
+
// still holds the anonymous id. Opt-out aware; failures are swallowed.
|
|
32
|
+
try {
|
|
33
|
+
const home = (0, paths_js_1.hicortexHome)();
|
|
34
|
+
const config = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(home, "config.json"), "utf-8"));
|
|
35
|
+
const version = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf-8")).version;
|
|
36
|
+
await (0, telemetry_js_1.sendLifecycleEvent)("uninstall", home, config, version);
|
|
37
|
+
}
|
|
38
|
+
catch { /* no config/state or unreadable — nothing to report */ }
|
|
29
39
|
console.log("Hicortex — Uninstall CC Integration\n");
|
|
30
40
|
const answer = await ask("This will remove Hicortex from Claude Code. Your memory database is preserved. Continue? [y/N] ");
|
|
31
41
|
if (answer.toLowerCase() !== "y") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.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": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"test": "vitest run",
|
|
39
39
|
"test:watch": "vitest",
|
|
40
40
|
"eval": "node dist/eval/run-eval.js",
|
|
41
|
+
"eval:recall-sweep": "node dist/eval/recall-sweep.js",
|
|
41
42
|
"prepack": "npm run build && rm -rf ./hermes-plugin && mkdir -p ./hermes-plugin && cp -r ../../hermes-plugin/hicortex ./hermes-plugin/ && find ./hermes-plugin -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null || true",
|
|
42
43
|
"prepublishOnly": "npm run build && rm -rf ./hermes-plugin && mkdir -p ./hermes-plugin && cp -r ../../hermes-plugin/hicortex ./hermes-plugin/ && find ./hermes-plugin -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null || true"
|
|
43
44
|
},
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"vitest": "^3.0.0"
|
|
50
51
|
},
|
|
51
52
|
"engines": {
|
|
52
|
-
"node": ">=
|
|
53
|
+
"node": ">=20"
|
|
53
54
|
},
|
|
54
55
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
55
56
|
"homepage": "https://hicortex.gamaze.com",
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
"dependencies": {
|
|
66
67
|
"@huggingface/transformers": "^3.0.0",
|
|
67
68
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
68
|
-
"better-sqlite3": "^11.
|
|
69
|
+
"better-sqlite3": "^12.11.1",
|
|
69
70
|
"express": "^4.21.0",
|
|
70
71
|
"sqlite-vec": "^0.1.7"
|
|
71
72
|
}
|