@gamaze/hicortex 0.15.0 → 0.15.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 +11 -3
- package/dist/cli.js +12 -0
- package/dist/consolidate.d.ts +0 -3
- package/dist/consolidate.js +7 -6
- package/dist/init.js +37 -0
- package/dist/mcp-server.js +5 -1
- package/dist/nightly.js +18 -1
- package/dist/retrieval.d.ts +32 -3
- package/dist/retrieval.js +98 -6
- package/dist/storage.js +10 -0
- package/dist/telemetry-cli.d.ts +11 -0
- package/dist/telemetry-cli.js +82 -0
- package/dist/telemetry.d.ts +53 -2
- package/dist/telemetry.js +65 -4
- package/dist/uninstall.js +10 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -56,9 +56,9 @@ The plugin connects to `http://127.0.0.1:8787` by default. For a remote server,
|
|
|
56
56
|
|
|
57
57
|
## Requirements
|
|
58
58
|
|
|
59
|
-
- Node.js
|
|
59
|
+
- Node.js 20+
|
|
60
60
|
- **Server mode:** LLM required — Ollama 9b+ (recommended), Claude CLI, or API key (Anthropic, OpenAI, etc.). ~500MB disk for database + embedding model.
|
|
61
|
-
- **Client mode:** No local LLM needed. Node.js
|
|
61
|
+
- **Client mode:** No local LLM needed. Node.js 20+ and network access to the server are sufficient.
|
|
62
62
|
- **OC plugin:** Requires a running Hicortex server. No local LLM, database, or embedder in the plugin itself.
|
|
63
63
|
|
|
64
64
|
## What Happens Automatically
|
|
@@ -164,6 +164,7 @@ npx @gamaze/hicortex context edit <name> # Edit a context section in $EDIT
|
|
|
164
164
|
npx @gamaze/hicortex context show --agent <id> # Show a specific agent's resolved context (0.13)
|
|
165
165
|
npx @gamaze/hicortex init --agent-name <name> # Opt in to a per-agent context id (default: unset — shared global context)
|
|
166
166
|
npx @gamaze/hicortex init --agent-name "" # Clear it back to global context
|
|
167
|
+
npx @gamaze/hicortex telemetry # Show exactly what anonymous telemetry sends
|
|
167
168
|
npx @gamaze/hicortex status # Show config, DB stats
|
|
168
169
|
npx @gamaze/hicortex uninstall # Remove CC integration (keeps DB)
|
|
169
170
|
```
|
|
@@ -209,6 +210,13 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
209
210
|
| `contextAgents` | Per-agent context modes (0.13): `{ "<id>": "override" \| "global" \| "off" }`. Absent + no `agents/<id>/` dir → every agent gets the global set. Boot-time (restart to apply) — see [Per-agent context](#per-agent-context-013) |
|
|
210
211
|
| `agentName` | This install's per-agent context id sent as `?agent=`. **Unset by default** (CC shares the global context — no `?agent=` sent). Explicit opt-in via `init --agent-name <name>`; `init --agent-name ""` clears it. An empty/whitespace value equals unset |
|
|
211
212
|
| `nightlyHour` | Local hour (0–23) for the nightly job installed by `init` (defaults: client 2, server 3). Applied on fresh installs; existing schedules are never overwritten |
|
|
213
|
+
| `scoreSimilarityWeight` | Weight of semantic similarity in the ranking score (default: 0.50) |
|
|
214
|
+
| `scoreStrengthWeight` | Weight of effective strength — importance/use/recency of access (default: 0.20) |
|
|
215
|
+
| `scoreConnectionsWeight` | Weight of graph centrality (default: 0.15) |
|
|
216
|
+
| `scoreRecencyWeight` | Weight of the slow recency curve (default: 0.15) |
|
|
217
|
+
| `freshnessBoostDays` | Fresh-memory window: new memories rank higher for this many days (default: 7) |
|
|
218
|
+
| `freshnessBoostWeight` | Size of the fresh-memory bonus at age 0, fading linearly to 0 at the window edge (default: 0.15; set 0 to disable) |
|
|
219
|
+
| `supersededDemotion` | Score multiplier for a memory a later decision reversed (default: 0.50) |
|
|
212
220
|
| `decayHalfLifeDays` | Memory decay half-life in days at reference importance (default: 365). Larger = slower forgetting; importance, access, and links slow it further |
|
|
213
221
|
| `searchLimit` / `recentLimit` | Default result counts for search (8) and recent (12) |
|
|
214
222
|
| `recentWindowDays` | Candidate window for recent recall (default: 180) |
|
|
@@ -221,7 +229,7 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
221
229
|
| `supersessionMinSimilarity` | Minimum cosine similarity for a nightly supersession candidate pair (default: 0.80) |
|
|
222
230
|
| `supersessionMaxCalls` | Max classify-tier LLM calls the nightly's supersession stage spends per run (default: 30) |
|
|
223
231
|
| `supersessionPenalty` | Multiplier applied to a superseded memory's `base_strength` (default: 0.5) |
|
|
224
|
-
| `telemetry` | Anonymous usage telemetry
|
|
232
|
+
| `telemetry` | Anonymous usage telemetry. **On by default and not written into config by `init`** — add `"telemetry": false` yourself (or set `HICORTEX_TELEMETRY=off`) to opt out. Inspect exactly what is sent with `hicortex telemetry` |
|
|
225
233
|
|
|
226
234
|
Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze.com/docs/configuration.html)
|
|
227
235
|
|
package/dist/cli.js
CHANGED
|
@@ -185,6 +185,17 @@ switch (command) {
|
|
|
185
185
|
});
|
|
186
186
|
break;
|
|
187
187
|
}
|
|
188
|
+
case "telemetry": {
|
|
189
|
+
// Transparency surface: reports what anonymous telemetry sends. Read-only
|
|
190
|
+
// by design — opting out is a deliberate config/env edit (0.15.1).
|
|
191
|
+
import("./telemetry-cli.js").then(({ runTelemetryCommand }) => {
|
|
192
|
+
runTelemetryCommand(process.argv.slice(3));
|
|
193
|
+
}).catch((err) => {
|
|
194
|
+
console.error("[hicortex] telemetry command failed:", err instanceof Error ? err.message : err);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
188
199
|
case "status":
|
|
189
200
|
import("./status.js").then(({ runStatus }) => {
|
|
190
201
|
runStatus().catch((err) => {
|
|
@@ -246,6 +257,7 @@ Commands:
|
|
|
246
257
|
lessons-context Fetch lessons and print Markdown to stdout (CC SessionStart hook)
|
|
247
258
|
recall-hook Pushed recall index for the current prompt (CC UserPromptSubmit/SessionStart hook)
|
|
248
259
|
context Standing context layer (show|edit) against the configured server
|
|
260
|
+
telemetry Show exactly what anonymous telemetry sends (read-only)
|
|
249
261
|
status Show current configuration and stats
|
|
250
262
|
uninstall Remove CC integration (preserves DB)
|
|
251
263
|
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -135,12 +135,9 @@ export declare function classifyRelationship(source: Memory, target: Memory, sim
|
|
|
135
135
|
export declare const DEFAULT_SUPERSESSION_MIN_SIMILARITY = 0.8;
|
|
136
136
|
/** Default max classify-tier LLM calls (pairs evaluated) spent per nightly run. */
|
|
137
137
|
export declare const DEFAULT_SUPERSESSION_MAX_CALLS = 30;
|
|
138
|
-
/** Default multiplier applied to a superseded memory's base_strength. */
|
|
139
|
-
export declare const DEFAULT_SUPERSESSION_PENALTY = 0.5;
|
|
140
138
|
export interface SupersessionOptions {
|
|
141
139
|
minSimilarity?: number;
|
|
142
140
|
maxCalls?: number;
|
|
143
|
-
penalty?: number;
|
|
144
141
|
}
|
|
145
142
|
export interface SupersessionStageResult {
|
|
146
143
|
scanned: number;
|
package/dist/consolidate.js
CHANGED
|
@@ -38,7 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
};
|
|
39
39
|
})();
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
-
exports.
|
|
41
|
+
exports.DEFAULT_SUPERSESSION_MAX_CALLS = exports.DEFAULT_SUPERSESSION_MIN_SIMILARITY = exports.BudgetTracker = exports.REFLECTION_CONTRADICTION_MIN_COSINE = exports.l2ToCosine = exports.CROSS_PROJECT_LINK_THRESHOLD = exports.CONSOLIDATE_LINK_TOP_K = exports.CONSOLIDATE_LINK_THRESHOLD = void 0;
|
|
42
42
|
exports.isContradictionCandidate = isContradictionCandidate;
|
|
43
43
|
exports.parseJsonLenient = parseJsonLenient;
|
|
44
44
|
exports.rebuildContentModuleIndex = rebuildContentModuleIndex;
|
|
@@ -850,9 +850,7 @@ exports.DEFAULT_SUPERSESSION_MIN_SIMILARITY = 0.8;
|
|
|
850
850
|
/** Default max classify-tier LLM calls (pairs evaluated) spent per nightly run. */
|
|
851
851
|
exports.DEFAULT_SUPERSESSION_MAX_CALLS = 30;
|
|
852
852
|
/** Default multiplier applied to a superseded memory's base_strength. */
|
|
853
|
-
exports.DEFAULT_SUPERSESSION_PENALTY = 0.5;
|
|
854
853
|
/** Floor under which a superseded memory's base_strength never drops. */
|
|
855
|
-
const SUPERSESSION_STRENGTH_FLOOR = 0.1;
|
|
856
854
|
/** Neighbor pool size before shape/older/similarity filtering narrows to top 5. */
|
|
857
855
|
const SUPERSESSION_NEIGHBOR_POOL = 15;
|
|
858
856
|
/** Older-neighbor pairs kept per candidate after filtering. */
|
|
@@ -965,7 +963,6 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
965
963
|
};
|
|
966
964
|
const minSimilarity = validNumber(options.minSimilarity, exports.DEFAULT_SUPERSESSION_MIN_SIMILARITY, (n) => n > 0 && n <= 1);
|
|
967
965
|
const maxCalls = validNumber(options.maxCalls, exports.DEFAULT_SUPERSESSION_MAX_CALLS, (n) => n >= 0);
|
|
968
|
-
const penalty = validNumber(options.penalty, exports.DEFAULT_SUPERSESSION_PENALTY, (n) => n > 0 && n <= 1);
|
|
969
966
|
const startCursor = (0, state_js_1.loadState)(stateDir).supersessionCursor ?? 0;
|
|
970
967
|
const rows = db
|
|
971
968
|
.prepare(`SELECT rowid AS __rowid, * FROM memories
|
|
@@ -1011,9 +1008,13 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1011
1008
|
}
|
|
1012
1009
|
if (verdict) {
|
|
1013
1010
|
const cosine = (0, retrieval_js_1.l2ToCosine)(neighbor.distance);
|
|
1011
|
+
// The link IS the signal (0.15.2): retrieval demotes superseded
|
|
1012
|
+
// memories via an explicit scoring multiplier (supersededDemotion,
|
|
1013
|
+
// retrieval.ts). The old base_strength penalty was retired because it
|
|
1014
|
+
// (a) fought the config-tunable strength weight and (b) leaked into
|
|
1015
|
+
// prune eligibility — a reversed decision must rank lower, not edge
|
|
1016
|
+
// toward deletion.
|
|
1014
1017
|
storage.addLink(db, neighbor.id, candidate.id, "superseded_by", cosine);
|
|
1015
|
-
const newStrength = Math.max(SUPERSESSION_STRENGTH_FLOOR, (neighbor.base_strength ?? 0.5) * penalty);
|
|
1016
|
-
storage.updateMemory(db, neighbor.id, { base_strength: newStrength });
|
|
1017
1018
|
superseded++;
|
|
1018
1019
|
console.log(`[hicortex] Supersession: ${neighbor.id.slice(0, 8)} superseded_by ${candidate.id.slice(0, 8)} (cosine ${cosine.toFixed(3)})`);
|
|
1019
1020
|
}
|
package/dist/init.js
CHANGED
|
@@ -31,6 +31,7 @@ exports.installRecallHooks = installRecallHooks;
|
|
|
31
31
|
exports.runInit = runInit;
|
|
32
32
|
exports.resolveNightlyHour = resolveNightlyHour;
|
|
33
33
|
const paths_js_1 = require("./paths.js");
|
|
34
|
+
const telemetry_js_1 = require("./telemetry.js");
|
|
34
35
|
const node_fs_1 = require("node:fs");
|
|
35
36
|
const node_path_1 = require("node:path");
|
|
36
37
|
const node_os_1 = require("node:os");
|
|
@@ -40,6 +41,24 @@ const node_crypto_1 = require("node:crypto");
|
|
|
40
41
|
const claude_md_js_1 = require("./claude-md.js");
|
|
41
42
|
const context_store_js_1 = require("./context-store.js");
|
|
42
43
|
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
44
|
+
/** This package's version, for the install lifecycle ping (0.15.2). */
|
|
45
|
+
function pkgVersion() {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf-8")).version;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return "0.0.0";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Read the just-written config so the install ping honours an opt-out. */
|
|
54
|
+
function readHomeConfig(home) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(home, "config.json"), "utf-8"));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
43
62
|
const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
|
|
44
63
|
const CC_COMMANDS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "commands");
|
|
45
64
|
const OC_CONFIG = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
|
|
@@ -1296,6 +1315,15 @@ async function runInit(options = {}) {
|
|
|
1296
1315
|
console.log(` ✓ Removed old static lessons block from ${claudeMdPath} — lessons now injected at session start`);
|
|
1297
1316
|
}
|
|
1298
1317
|
console.log("\n✓ Hicortex setup complete!\n");
|
|
1318
|
+
// Telemetry disclosure at install time (informed consent, best practice):
|
|
1319
|
+
// opt-out telemetry is only acceptable if the user is TOLD about it.
|
|
1320
|
+
console.log("Anonymous usage telemetry (aggregate counts only, no content) is on by default —");
|
|
1321
|
+
console.log("see exactly what is sent with `hicortex telemetry`; to opt out, add");
|
|
1322
|
+
console.log('"telemetry": false to ~/.hicortex/config.json or set HICORTEX_TELEMETRY=off.\n');
|
|
1323
|
+
// Install ping (0.15.2) — sent AFTER the disclosure above, opt-out aware, so
|
|
1324
|
+
// the install → first-nightly → retained funnel is measurable. Never blocks:
|
|
1325
|
+
// failures are swallowed inside sendLifecycleEvent.
|
|
1326
|
+
await (0, telemetry_js_1.sendLifecycleEvent)("install", HICORTEX_HOME, readHomeConfig(HICORTEX_HOME), pkgVersion());
|
|
1299
1327
|
console.log("Next steps:");
|
|
1300
1328
|
console.log(" 1. Restart Claude Code to pick up the new MCP server and SessionStart hook");
|
|
1301
1329
|
if (d.hermesFound) {
|
|
@@ -1461,6 +1489,15 @@ async function runClientInit(serverUrl, agentName) {
|
|
|
1461
1489
|
setupHermes(serverUrl, authToken);
|
|
1462
1490
|
}
|
|
1463
1491
|
console.log("\n✓ Hicortex client setup complete!\n");
|
|
1492
|
+
// Telemetry disclosure at install time (informed consent, best practice):
|
|
1493
|
+
// opt-out telemetry is only acceptable if the user is TOLD about it.
|
|
1494
|
+
console.log("Anonymous usage telemetry (aggregate counts only, no content) is on by default —");
|
|
1495
|
+
console.log("see exactly what is sent with `hicortex telemetry`; to opt out, add");
|
|
1496
|
+
console.log('"telemetry": false to ~/.hicortex/config.json or set HICORTEX_TELEMETRY=off.\n');
|
|
1497
|
+
// Install ping (0.15.2) — sent AFTER the disclosure above, opt-out aware, so
|
|
1498
|
+
// the install → first-nightly → retained funnel is measurable. Never blocks:
|
|
1499
|
+
// failures are swallowed inside sendLifecycleEvent.
|
|
1500
|
+
await (0, telemetry_js_1.sendLifecycleEvent)("install", HICORTEX_HOME, readHomeConfig(HICORTEX_HOME), pkgVersion());
|
|
1464
1501
|
console.log("How it works:");
|
|
1465
1502
|
console.log(" • MCP tools (search, context, ingest) talk to the remote server");
|
|
1466
1503
|
console.log(" • Nightly pipeline denoises CC transcripts, POSTs to server for distillation");
|
package/dist/mcp-server.js
CHANGED
|
@@ -487,8 +487,12 @@ async function startServer(options = {}) {
|
|
|
487
487
|
// so calibration is a config edit + restart, never a release.
|
|
488
488
|
retrieval.configureDecay({ halfLifeDays: savedConfig?.decayHalfLifeDays });
|
|
489
489
|
const recallCfg = retrieval.configureRecall(savedConfig);
|
|
490
|
+
const scoringCfg = retrieval.configureScoring(savedConfig);
|
|
490
491
|
console.log(`[hicortex] Recall: k=${recallCfg.searchLimit}/recent=${recallCfg.recentLimit}` +
|
|
491
|
-
`/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots}`
|
|
492
|
+
`/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots} · ` +
|
|
493
|
+
`score sim=${scoringCfg.similarity}/str=${scoringCfg.strength}/conn=${scoringCfg.connections}` +
|
|
494
|
+
`/rec=${scoringCfg.recency}, fresh=${scoringCfg.freshnessBoostWeight}@${scoringCfg.freshnessBoostDays}d, ` +
|
|
495
|
+
`superseded×${scoringCfg.supersededDemotion}`);
|
|
492
496
|
recallRegistry = new recall_registry_js_1.SessionRecallRegistry({
|
|
493
497
|
reshowTurns: savedConfig?.recallReshowTurns,
|
|
494
498
|
});
|
package/dist/nightly.js
CHANGED
|
@@ -214,6 +214,7 @@ async function runNightly(options = {}) {
|
|
|
214
214
|
// the server's retrieval path (config decayHalfLifeDays, default 365).
|
|
215
215
|
(0, retrieval_js_1.configureDecay)({ halfLifeDays: savedConfig?.decayHalfLifeDays });
|
|
216
216
|
(0, retrieval_js_1.configureRecall)(savedConfig);
|
|
217
|
+
(0, retrieval_js_1.configureScoring)(savedConfig);
|
|
217
218
|
const modeLabel = captureOnly ? " (capture-only)" : dryRun ? " (dry run)" : "";
|
|
218
219
|
console.log(`[hicortex] Nightly pipeline starting${modeLabel}`);
|
|
219
220
|
if (captureOnly) {
|
|
@@ -393,7 +394,6 @@ async function runNightly(options = {}) {
|
|
|
393
394
|
}, {
|
|
394
395
|
minSimilarity: savedConfig?.supersessionMinSimilarity,
|
|
395
396
|
maxCalls: savedConfig?.supersessionMaxCalls,
|
|
396
|
-
penalty: savedConfig?.supersessionPenalty,
|
|
397
397
|
});
|
|
398
398
|
console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
|
|
399
399
|
(report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
|
|
@@ -423,15 +423,29 @@ async function runNightly(options = {}) {
|
|
|
423
423
|
ocBatches.length > 0 && "oc",
|
|
424
424
|
].filter(Boolean);
|
|
425
425
|
const agentType = kinds.length > 1 ? "mixed" : (kinds[0] ?? "cc");
|
|
426
|
+
// Adoption aggregates (0.15.1): corpus-wide exposure vs use. uses/shown
|
|
427
|
+
// is the recall-quality signal; cold is the never-touched share.
|
|
428
|
+
const adoption = db
|
|
429
|
+
.prepare(`SELECT COALESCE(SUM(shown_count), 0) AS shown,
|
|
430
|
+
COALESCE(SUM(access_count), 0) AS uses,
|
|
431
|
+
SUM(CASE WHEN COALESCE(shown_count, 0) = 0
|
|
432
|
+
AND COALESCE(access_count, 0) = 0 THEN 1 ELSE 0 END) AS cold
|
|
433
|
+
FROM memories`)
|
|
434
|
+
.get();
|
|
426
435
|
await (0, telemetry_js_1.sendTelemetry)({
|
|
427
436
|
id: (0, telemetry_js_1.getTelemetryId)(stateDir),
|
|
428
437
|
v: VERSION,
|
|
438
|
+
pv: telemetry_js_1.TELEMETRY_PAYLOAD_VERSION,
|
|
439
|
+
event: "nightly",
|
|
429
440
|
mode: "server",
|
|
430
441
|
agent: agentType,
|
|
431
442
|
mem: storage.countMemories(db),
|
|
432
443
|
lessons: storage.getLessons(db, 365).length,
|
|
433
444
|
sessions: batches.length,
|
|
434
445
|
ok: !hadTransientFailure,
|
|
446
|
+
shown: adoption.shown,
|
|
447
|
+
uses: adoption.uses,
|
|
448
|
+
cold: adoption.cold,
|
|
435
449
|
});
|
|
436
450
|
}
|
|
437
451
|
}
|
|
@@ -554,12 +568,15 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
|
|
|
554
568
|
await (0, telemetry_js_1.sendTelemetry)({
|
|
555
569
|
id: (0, telemetry_js_1.getTelemetryId)(stateDir),
|
|
556
570
|
v: VERSION,
|
|
571
|
+
pv: telemetry_js_1.TELEMETRY_PAYLOAD_VERSION,
|
|
572
|
+
event: "nightly",
|
|
557
573
|
mode: "client",
|
|
558
574
|
agent: agentType,
|
|
559
575
|
mem: memoriesIngested,
|
|
560
576
|
lessons: 0, // client doesn't have direct DB access
|
|
561
577
|
sessions: batches.length,
|
|
562
578
|
ok: !hadTransientFailure,
|
|
579
|
+
// No adoption fields: a client install has no local DB to aggregate.
|
|
563
580
|
});
|
|
564
581
|
}
|
|
565
582
|
}
|
package/dist/retrieval.d.ts
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Retrieval layer with composite scoring, RRF fusion, and graph traversal.
|
|
3
3
|
* Ported from hicortex/retrieval.py — same scoring model and weights.
|
|
4
4
|
*
|
|
5
|
-
* Scoring model:
|
|
6
|
-
* score = similarity * 0.
|
|
5
|
+
* Scoring model (weights are config-driven since 0.15.2 — see configureScoring):
|
|
6
|
+
* score = similarity * 0.50 + effective_strength * 0.20
|
|
7
|
+
* + connection_score * 0.15 + recency * 0.15
|
|
8
|
+
* + fresh-memory bonus (≤ 0.15, linear over the first 7 days)
|
|
9
|
+
* then × 0.50 if the memory was superseded by a later decision
|
|
7
10
|
*
|
|
8
11
|
* Decay model (B+E+D):
|
|
9
12
|
* base_decay = derived from decayHalfLifeDays (config; default 365 → ~1-year
|
|
@@ -49,6 +52,30 @@ interface RecallDefaults {
|
|
|
49
52
|
* Returns the resolved values (for logging + tests).
|
|
50
53
|
*/
|
|
51
54
|
export declare function configureRecall(config?: Record<string, unknown> | null): RecallDefaults;
|
|
55
|
+
interface ScoringWeights {
|
|
56
|
+
similarity: number;
|
|
57
|
+
strength: number;
|
|
58
|
+
connections: number;
|
|
59
|
+
recency: number;
|
|
60
|
+
freshnessBoostDays: number;
|
|
61
|
+
freshnessBoostWeight: number;
|
|
62
|
+
supersededDemotion: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Configure scoring weights + ranking knobs from config. Called at boot by the
|
|
66
|
+
* server and the nightly (alongside configureDecay/configureRecall) so
|
|
67
|
+
* retrieval and consolidation rank identically. Invalid/absent values keep the
|
|
68
|
+
* shipped default per key. Returns the resolved set for logging/tests.
|
|
69
|
+
*/
|
|
70
|
+
export declare function configureScoring(config?: Record<string, unknown> | null): ScoringWeights;
|
|
71
|
+
/** Current resolved weights (tests + status output). */
|
|
72
|
+
export declare function getScoringWeights(): ScoringWeights;
|
|
73
|
+
/**
|
|
74
|
+
* Ids among `candidateIds` that have been superseded by a later memory — i.e.
|
|
75
|
+
* they are the SOURCE of a `superseded_by` link (stageSupersession links
|
|
76
|
+
* old → new). One query, not per-candidate.
|
|
77
|
+
*/
|
|
78
|
+
export declare function findSupersededIds(db: Database.Database, candidateIds: string[]): Set<string>;
|
|
52
79
|
/**
|
|
53
80
|
* Convert an L2 distance (as returned by sqlite-vec's vec0 `distance`) to
|
|
54
81
|
* cosine similarity. Valid because our embeddings are L2-normalized
|
|
@@ -72,7 +99,9 @@ export declare function effectiveStrength(baseStrength: number, lastAccessed: st
|
|
|
72
99
|
* Return a composite relevance score in [0, 1] for a candidate memory.
|
|
73
100
|
* Exported for exact-value tests of the similarity component (#145).
|
|
74
101
|
*/
|
|
75
|
-
export declare function computeScore(memory: Memory, distance: number, connectionCount: number, maxConnections: number, now: Date
|
|
102
|
+
export declare function computeScore(memory: Memory, distance: number, connectionCount: number, maxConnections: number, now: Date, options?: {
|
|
103
|
+
superseded?: boolean;
|
|
104
|
+
}): number;
|
|
76
105
|
export interface EmbedFn {
|
|
77
106
|
(text: string): Promise<Float32Array>;
|
|
78
107
|
}
|
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
|
|
@@ -53,6 +56,9 @@ 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.findSupersededIds = findSupersededIds;
|
|
56
62
|
exports.l2ToCosine = l2ToCosine;
|
|
57
63
|
exports.effectiveStrength = effectiveStrength;
|
|
58
64
|
exports.computeScore = computeScore;
|
|
@@ -110,6 +116,57 @@ function configureRecall(config) {
|
|
|
110
116
|
};
|
|
111
117
|
return { ...recallDefaults };
|
|
112
118
|
}
|
|
119
|
+
const SCORING_DEFAULTS = {
|
|
120
|
+
similarity: 0.5,
|
|
121
|
+
strength: 0.2,
|
|
122
|
+
connections: 0.15,
|
|
123
|
+
recency: 0.15,
|
|
124
|
+
freshnessBoostDays: 7,
|
|
125
|
+
freshnessBoostWeight: 0.15,
|
|
126
|
+
supersededDemotion: 0.5,
|
|
127
|
+
};
|
|
128
|
+
let scoringWeights = { ...SCORING_DEFAULTS };
|
|
129
|
+
/**
|
|
130
|
+
* Configure scoring weights + ranking knobs from config. Called at boot by the
|
|
131
|
+
* server and the nightly (alongside configureDecay/configureRecall) so
|
|
132
|
+
* retrieval and consolidation rank identically. Invalid/absent values keep the
|
|
133
|
+
* shipped default per key. Returns the resolved set for logging/tests.
|
|
134
|
+
*/
|
|
135
|
+
function configureScoring(config) {
|
|
136
|
+
const num = (key, dflt, min, max) => {
|
|
137
|
+
const v = Number(config?.[key]);
|
|
138
|
+
return Number.isFinite(v) && v >= min && v <= max ? v : dflt;
|
|
139
|
+
};
|
|
140
|
+
scoringWeights = {
|
|
141
|
+
similarity: num("scoreSimilarityWeight", SCORING_DEFAULTS.similarity, 0, 1),
|
|
142
|
+
strength: num("scoreStrengthWeight", SCORING_DEFAULTS.strength, 0, 1),
|
|
143
|
+
connections: num("scoreConnectionsWeight", SCORING_DEFAULTS.connections, 0, 1),
|
|
144
|
+
recency: num("scoreRecencyWeight", SCORING_DEFAULTS.recency, 0, 1),
|
|
145
|
+
freshnessBoostDays: num("freshnessBoostDays", SCORING_DEFAULTS.freshnessBoostDays, 0, 365),
|
|
146
|
+
freshnessBoostWeight: num("freshnessBoostWeight", SCORING_DEFAULTS.freshnessBoostWeight, 0, 1),
|
|
147
|
+
supersededDemotion: num("supersededDemotion", SCORING_DEFAULTS.supersededDemotion, 0, 1),
|
|
148
|
+
};
|
|
149
|
+
return { ...scoringWeights };
|
|
150
|
+
}
|
|
151
|
+
/** Current resolved weights (tests + status output). */
|
|
152
|
+
function getScoringWeights() {
|
|
153
|
+
return { ...scoringWeights };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Ids among `candidateIds` that have been superseded by a later memory — i.e.
|
|
157
|
+
* they are the SOURCE of a `superseded_by` link (stageSupersession links
|
|
158
|
+
* old → new). One query, not per-candidate.
|
|
159
|
+
*/
|
|
160
|
+
function findSupersededIds(db, candidateIds) {
|
|
161
|
+
if (candidateIds.length === 0)
|
|
162
|
+
return new Set();
|
|
163
|
+
const placeholders = candidateIds.map(() => "?").join(",");
|
|
164
|
+
const rows = db
|
|
165
|
+
.prepare(`SELECT DISTINCT source_id FROM memory_links
|
|
166
|
+
WHERE relationship = 'superseded_by' AND source_id IN (${placeholders})`)
|
|
167
|
+
.all(...candidateIds);
|
|
168
|
+
return new Set(rows.map((r) => r.source_id));
|
|
169
|
+
}
|
|
113
170
|
/**
|
|
114
171
|
* Placeholder L2 distance for candidates that have no measured vector
|
|
115
172
|
* distance (FTS-only hits and graph-discovered neighbors). Chosen so that
|
|
@@ -175,7 +232,7 @@ function effectiveStrength(baseStrength, lastAccessed, now, options) {
|
|
|
175
232
|
* Return a composite relevance score in [0, 1] for a candidate memory.
|
|
176
233
|
* Exported for exact-value tests of the similarity component (#145).
|
|
177
234
|
*/
|
|
178
|
-
function computeScore(memory, distance, connectionCount, maxConnections, now) {
|
|
235
|
+
function computeScore(memory, distance, connectionCount, maxConnections, now, options) {
|
|
179
236
|
// TRUE cosine similarity (#145). The old `1 − distance` compressed real
|
|
180
237
|
// cosines (cos 0.8 scored 0.37) and the 0-clamp at that scale flattened
|
|
181
238
|
// everything below cos 0.5 to exactly 0, killing mid-relevance
|
|
@@ -193,7 +250,34 @@ function computeScore(memory, distance, connectionCount, maxConnections, now) {
|
|
|
193
250
|
const connScore = maxConnections > 0 ? connectionCount / maxConnections : 0;
|
|
194
251
|
const hoursSinceCreated = Math.max((now.getTime() - parseTimestamp(memory.created_at).getTime()) / 3_600_000, 0);
|
|
195
252
|
const recency = Math.pow(0.9995, hoursSinceCreated);
|
|
196
|
-
|
|
253
|
+
const w = scoringWeights;
|
|
254
|
+
let score = similarity * w.similarity +
|
|
255
|
+
effStrength * w.strength +
|
|
256
|
+
connScore * w.connections +
|
|
257
|
+
recency * w.recency;
|
|
258
|
+
// Fresh-memory window (#191 Phase B): a memory is born highly available and
|
|
259
|
+
// settles into the normal ranking over `freshnessBoostDays`. Age is measured
|
|
260
|
+
// from created_at, which the nightly sets from the session's own date — so a
|
|
261
|
+
// session captured last night ranks as ~1 day old (not 0), and backfilled
|
|
262
|
+
// older content correctly gets no boost. The slow
|
|
263
|
+
// `recency` term above (≈58-day half-life at weight 0.15) could never lift a
|
|
264
|
+
// day-old memory past a hardened old one — measured case: an exact-match
|
|
265
|
+
// 1-day-old memory (strength 0.50) lost to an unrelated memory at strength
|
|
266
|
+
// 0.80. This is an ADDITIVE bonus that decays linearly to zero at the window
|
|
267
|
+
// edge, so it cannot distort ranking among memories that are all old.
|
|
268
|
+
const ageDays = hoursSinceCreated / 24;
|
|
269
|
+
if (memory.created_at && ageDays < scoringWeights.freshnessBoostDays) {
|
|
270
|
+
const freshness = 1 - ageDays / scoringWeights.freshnessBoostDays;
|
|
271
|
+
score += freshness * scoringWeights.freshnessBoostWeight;
|
|
272
|
+
}
|
|
273
|
+
// Superseded demotion (#191 Phase B): a memory whose decision was reversed by
|
|
274
|
+
// a later one keeps its content and strength but must not outrank the
|
|
275
|
+
// decision that replaced it. Applied as an explicit multiplier here rather
|
|
276
|
+
// than by penalizing base_strength, so ranking weights stay independently
|
|
277
|
+
// tunable and supersession never nudges a memory toward prune eligibility.
|
|
278
|
+
if (options?.superseded)
|
|
279
|
+
score *= scoringWeights.supersededDemotion;
|
|
280
|
+
return Math.max(0, Math.min(1, score));
|
|
197
281
|
}
|
|
198
282
|
// ---------------------------------------------------------------------------
|
|
199
283
|
// Graph traversal
|
|
@@ -357,9 +441,14 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
357
441
|
: [0]));
|
|
358
442
|
const scored = [];
|
|
359
443
|
const maxRrf = Math.max(...([...rrfScores.values()].length > 0 ? [...rrfScores.values()] : [1]));
|
|
444
|
+
// One query for the whole candidate set (#191 Phase B): superseded memories
|
|
445
|
+
// are demoted in computeScore rather than strength-penalized.
|
|
446
|
+
const supersededIds = findSupersededIds(db, [...candidateMap.keys()]);
|
|
360
447
|
for (const [mid, { mem, distance, source }] of candidateMap) {
|
|
361
448
|
const connCount = connectionCounts.get(mid) ?? 0;
|
|
362
|
-
const composite = computeScore(mem, distance, connCount, maxConnections, now
|
|
449
|
+
const composite = computeScore(mem, distance, connCount, maxConnections, now, {
|
|
450
|
+
superseded: supersededIds.has(mid),
|
|
451
|
+
});
|
|
363
452
|
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now, {
|
|
364
453
|
accessCount: mem.access_count ?? 0,
|
|
365
454
|
linkCount: connectionCounts.get(mem.id) ?? 0,
|
|
@@ -435,9 +524,12 @@ function searchRecent(db, options) {
|
|
|
435
524
|
? [...connectionCounts.values()]
|
|
436
525
|
: [0]));
|
|
437
526
|
const scored = [];
|
|
527
|
+
const supersededRecent = findSupersededIds(db, candidates.map((c) => c.id));
|
|
438
528
|
for (const mem of candidates) {
|
|
439
529
|
const connCount = connectionCounts.get(mem.id) ?? 0;
|
|
440
|
-
const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, maxConnections, now
|
|
530
|
+
const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, maxConnections, now, {
|
|
531
|
+
superseded: supersededRecent.has(mem.id),
|
|
532
|
+
});
|
|
441
533
|
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now, {
|
|
442
534
|
accessCount: mem.access_count ?? 0,
|
|
443
535
|
linkCount: connectionCounts.get(mem.id) ?? 0,
|
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());
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hicortex telemetry` — transparency surface for anonymous usage telemetry.
|
|
3
|
+
*
|
|
4
|
+
* Read-only BY DESIGN (owner decision 30.07.2026). It shows the exact payload
|
|
5
|
+
* and both documented ways to switch telemetry off, but it does not flip the
|
|
6
|
+
* switch itself: the `telemetry` key is deliberately NOT scaffolded into
|
|
7
|
+
* config.json, and opting out is a deliberate edit the operator makes. Turning
|
|
8
|
+
* it off must stay completely possible and completely documented — just not a
|
|
9
|
+
* one-keystroke default-path action.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runTelemetryCommand(args: string[]): void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `hicortex telemetry` — transparency surface for anonymous usage telemetry.
|
|
4
|
+
*
|
|
5
|
+
* Read-only BY DESIGN (owner decision 30.07.2026). It shows the exact payload
|
|
6
|
+
* and both documented ways to switch telemetry off, but it does not flip the
|
|
7
|
+
* switch itself: the `telemetry` key is deliberately NOT scaffolded into
|
|
8
|
+
* config.json, and opting out is a deliberate edit the operator makes. Turning
|
|
9
|
+
* it off must stay completely possible and completely documented — just not a
|
|
10
|
+
* one-keystroke default-path action.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.runTelemetryCommand = runTelemetryCommand;
|
|
14
|
+
const node_fs_1 = require("node:fs");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const paths_js_1 = require("./paths.js");
|
|
17
|
+
const telemetry_js_1 = require("./telemetry.js");
|
|
18
|
+
function readConfig(home) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(home, "config.json"), "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function printHowToDisable(home) {
|
|
27
|
+
console.log("To turn it off (either one works, both are permanent):");
|
|
28
|
+
console.log(` 1. add "telemetry": false to ${(0, node_path_1.join)(home, "config.json")}`);
|
|
29
|
+
console.log(" 2. or set HICORTEX_TELEMETRY=off in the environment");
|
|
30
|
+
}
|
|
31
|
+
function runTelemetryCommand(args) {
|
|
32
|
+
const home = (0, paths_js_1.hicortexHome)();
|
|
33
|
+
const sub = args[0] ?? "status";
|
|
34
|
+
if (sub === "on" || sub === "off") {
|
|
35
|
+
// Intentionally not a write command — see the module docstring.
|
|
36
|
+
console.log(`[hicortex] telemetry is not toggled by this command; it reports state only.`);
|
|
37
|
+
printHowToDisable(home);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (sub !== "status") {
|
|
41
|
+
console.error(`[hicortex] telemetry: unknown subcommand '${sub}' (only 'status' is supported)`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const config = readConfig(home);
|
|
45
|
+
const reason = (0, telemetry_js_1.telemetryDisabledReason)(config);
|
|
46
|
+
const mode = config?.mode === "client" ? "client" : "server";
|
|
47
|
+
console.log("Hicortex telemetry");
|
|
48
|
+
console.log("──────────────────────────────────────────");
|
|
49
|
+
if (reason) {
|
|
50
|
+
console.log(`Status: DISABLED (via ${reason === "env" ? "HICORTEX_TELEMETRY env var" : 'config.json "telemetry": false'})`);
|
|
51
|
+
console.log("Nothing is sent. Remove that setting to re-enable.");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.log("Status: ENABLED (anonymous, aggregate only) — the default");
|
|
55
|
+
console.log(`Endpoint: ${telemetry_js_1.TELEMETRY_URL}`);
|
|
56
|
+
console.log("When: once at the end of each full nightly run");
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log("Exactly what is sent (counts from this install; values vary per run):");
|
|
59
|
+
const example = {
|
|
60
|
+
id: (0, telemetry_js_1.getTelemetryId)(home),
|
|
61
|
+
v: "<package version>",
|
|
62
|
+
pv: telemetry_js_1.TELEMETRY_PAYLOAD_VERSION,
|
|
63
|
+
mode,
|
|
64
|
+
agent: "<cc|hermes|pi|oc|mixed>",
|
|
65
|
+
mem: "<total memories>",
|
|
66
|
+
lessons: "<total lessons>",
|
|
67
|
+
sessions: "<sessions captured this run>",
|
|
68
|
+
ok: "<nightly succeeded>",
|
|
69
|
+
};
|
|
70
|
+
if (mode === "server") {
|
|
71
|
+
example.shown = "<sum of shown_count>";
|
|
72
|
+
example.uses = "<sum of access_count>";
|
|
73
|
+
example.cold = "<memories never shown or used>";
|
|
74
|
+
}
|
|
75
|
+
console.log(JSON.stringify(example, null, 2));
|
|
76
|
+
console.log("");
|
|
77
|
+
console.log("NOT sent: memory content, prompts, file paths, project names,");
|
|
78
|
+
console.log("hostnames, tokens, or IP addresses (the server stores no IPs).");
|
|
79
|
+
console.log("Every install sends the same fields — nothing marks yours as special.");
|
|
80
|
+
console.log("");
|
|
81
|
+
printHowToDisable(home);
|
|
82
|
+
}
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Anonymous telemetry — sends aggregate stats after each nightly run.
|
|
3
3
|
*
|
|
4
|
-
* What's sent (
|
|
4
|
+
* What's sent (all aggregate, payload version 2 since 0.15.1):
|
|
5
5
|
* id — random UUID, generated once on first run, stored in state.json
|
|
6
6
|
* v — package version
|
|
7
|
+
* pv — payload schema version
|
|
7
8
|
* mode — server or client
|
|
8
9
|
* agent — cc, pi, oc, or mixed (detected from session sources)
|
|
9
10
|
* mem — total memory count
|
|
10
11
|
* lessons — total lesson count
|
|
11
12
|
* sessions — sessions distilled this run
|
|
12
13
|
* ok — nightly succeeded (true/false)
|
|
14
|
+
* shown — sum of shown_count (server mode only)
|
|
15
|
+
* uses — sum of access_count (server mode only)
|
|
16
|
+
* cold — memories never shown and never used (server mode only)
|
|
17
|
+
* event — install | nightly | uninstall (which lifecycle moment this is)
|
|
18
|
+
*
|
|
19
|
+
* Every install sends the SAME fields — nothing marks an install as special
|
|
20
|
+
* (a rare label would be a fingerprint, i.e. no longer anonymous). Excluding
|
|
21
|
+
* the maintainer's own installs from adoption stats is an analysis-side
|
|
22
|
+
* concern, done by anonymous id at the admin endpoint.
|
|
13
23
|
*
|
|
14
24
|
* What's NOT sent:
|
|
15
|
-
* No personal data, no session content, no file paths, no
|
|
25
|
+
* No personal data, no session content, no file paths, no project names,
|
|
26
|
+
* no hostnames, no tokens. The server stores no IPs.
|
|
27
|
+
*
|
|
28
|
+
* Inspect / control:
|
|
29
|
+
* `hicortex telemetry` prints the exact payload shape and current state;
|
|
30
|
+
* `hicortex telemetry off` / `on` flips it.
|
|
16
31
|
*
|
|
17
32
|
* Opt-out:
|
|
18
33
|
* Set "telemetry": false in ~/.hicortex/config.json
|
|
@@ -21,6 +36,7 @@
|
|
|
21
36
|
* The ping is fire-and-forget with a 5s timeout. If it fails, nothing
|
|
22
37
|
* happens — the nightly result is unaffected.
|
|
23
38
|
*/
|
|
39
|
+
export declare const TELEMETRY_URL = "https://hicortex.gamaze.com/api/telemetry";
|
|
24
40
|
export interface TelemetryPayload {
|
|
25
41
|
id: string;
|
|
26
42
|
v: string;
|
|
@@ -30,6 +46,25 @@ export interface TelemetryPayload {
|
|
|
30
46
|
lessons: number;
|
|
31
47
|
sessions: number;
|
|
32
48
|
ok: boolean;
|
|
49
|
+
/** Payload schema version (2 = adoption fields, 0.15.1). Absent = v1. */
|
|
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";
|
|
58
|
+
/**
|
|
59
|
+
* Adoption aggregates (server mode only — a client install has no DB).
|
|
60
|
+
* `shown`/`uses` are corpus-wide sums of shown_count/access_count; their
|
|
61
|
+
* ratio (uses per showing) is the recall-quality signal. `cold` counts
|
|
62
|
+
* memories never shown AND never used. Aggregate counts only — no content,
|
|
63
|
+
* no ids, nothing per-memory.
|
|
64
|
+
*/
|
|
65
|
+
shown?: number;
|
|
66
|
+
uses?: number;
|
|
67
|
+
cold?: number;
|
|
33
68
|
}
|
|
34
69
|
/**
|
|
35
70
|
* Check if telemetry is enabled. Disabled by:
|
|
@@ -42,7 +77,23 @@ export declare function isTelemetryEnabled(config: Record<string, unknown> | nul
|
|
|
42
77
|
* Generated once, stored in state.json, never linked to any personal info.
|
|
43
78
|
*/
|
|
44
79
|
export declare function getTelemetryId(stateDir: string): string;
|
|
80
|
+
/** Payload schema version sent by this release. */
|
|
81
|
+
export declare const TELEMETRY_PAYLOAD_VERSION = 2;
|
|
82
|
+
/**
|
|
83
|
+
* Why telemetry is off, or null when it is on. Exposed so `hicortex telemetry`
|
|
84
|
+
* can tell the operator WHICH switch is in effect (config vs env).
|
|
85
|
+
*/
|
|
86
|
+
export declare function telemetryDisabledReason(config: Record<string, unknown> | null): "config" | "env" | null;
|
|
45
87
|
/**
|
|
46
88
|
* Send anonymous telemetry. Fire-and-forget — failures are silently ignored.
|
|
47
89
|
*/
|
|
48
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
|
@@ -2,18 +2,33 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Anonymous telemetry — sends aggregate stats after each nightly run.
|
|
4
4
|
*
|
|
5
|
-
* What's sent (
|
|
5
|
+
* What's sent (all aggregate, payload version 2 since 0.15.1):
|
|
6
6
|
* id — random UUID, generated once on first run, stored in state.json
|
|
7
7
|
* v — package version
|
|
8
|
+
* pv — payload schema version
|
|
8
9
|
* mode — server or client
|
|
9
10
|
* agent — cc, pi, oc, or mixed (detected from session sources)
|
|
10
11
|
* mem — total memory count
|
|
11
12
|
* lessons — total lesson count
|
|
12
13
|
* sessions — sessions distilled this run
|
|
13
14
|
* ok — nightly succeeded (true/false)
|
|
15
|
+
* shown — sum of shown_count (server mode only)
|
|
16
|
+
* uses — sum of access_count (server mode only)
|
|
17
|
+
* cold — memories never shown and never used (server mode only)
|
|
18
|
+
* event — install | nightly | uninstall (which lifecycle moment this is)
|
|
19
|
+
*
|
|
20
|
+
* Every install sends the SAME fields — nothing marks an install as special
|
|
21
|
+
* (a rare label would be a fingerprint, i.e. no longer anonymous). Excluding
|
|
22
|
+
* the maintainer's own installs from adoption stats is an analysis-side
|
|
23
|
+
* concern, done by anonymous id at the admin endpoint.
|
|
14
24
|
*
|
|
15
25
|
* What's NOT sent:
|
|
16
|
-
* No personal data, no session content, no file paths, no
|
|
26
|
+
* No personal data, no session content, no file paths, no project names,
|
|
27
|
+
* no hostnames, no tokens. The server stores no IPs.
|
|
28
|
+
*
|
|
29
|
+
* Inspect / control:
|
|
30
|
+
* `hicortex telemetry` prints the exact payload shape and current state;
|
|
31
|
+
* `hicortex telemetry off` / `on` flips it.
|
|
17
32
|
*
|
|
18
33
|
* Opt-out:
|
|
19
34
|
* Set "telemetry": false in ~/.hicortex/config.json
|
|
@@ -23,12 +38,15 @@
|
|
|
23
38
|
* happens — the nightly result is unaffected.
|
|
24
39
|
*/
|
|
25
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.TELEMETRY_PAYLOAD_VERSION = exports.TELEMETRY_URL = void 0;
|
|
26
42
|
exports.isTelemetryEnabled = isTelemetryEnabled;
|
|
27
43
|
exports.getTelemetryId = getTelemetryId;
|
|
44
|
+
exports.telemetryDisabledReason = telemetryDisabledReason;
|
|
28
45
|
exports.sendTelemetry = sendTelemetry;
|
|
46
|
+
exports.sendLifecycleEvent = sendLifecycleEvent;
|
|
29
47
|
const node_crypto_1 = require("node:crypto");
|
|
30
48
|
const state_js_1 = require("./state.js");
|
|
31
|
-
|
|
49
|
+
exports.TELEMETRY_URL = "https://hicortex.gamaze.com/api/telemetry";
|
|
32
50
|
/**
|
|
33
51
|
* Check if telemetry is enabled. Disabled by:
|
|
34
52
|
* - config.telemetry === false
|
|
@@ -59,10 +77,24 @@ function getTelemetryId(stateDir) {
|
|
|
59
77
|
}, stateDir);
|
|
60
78
|
return id;
|
|
61
79
|
}
|
|
80
|
+
/** Payload schema version sent by this release. */
|
|
81
|
+
exports.TELEMETRY_PAYLOAD_VERSION = 2;
|
|
82
|
+
/**
|
|
83
|
+
* Why telemetry is off, or null when it is on. Exposed so `hicortex telemetry`
|
|
84
|
+
* can tell the operator WHICH switch is in effect (config vs env).
|
|
85
|
+
*/
|
|
86
|
+
function telemetryDisabledReason(config) {
|
|
87
|
+
const env = process.env.HICORTEX_TELEMETRY?.toLowerCase();
|
|
88
|
+
if (env === "off" || env === "false" || env === "0")
|
|
89
|
+
return "env";
|
|
90
|
+
if (config?.telemetry === false)
|
|
91
|
+
return "config";
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
62
94
|
/**
|
|
63
95
|
* Send anonymous telemetry. Fire-and-forget — failures are silently ignored.
|
|
64
96
|
*/
|
|
65
|
-
async function sendTelemetry(payload, serverUrl = TELEMETRY_URL) {
|
|
97
|
+
async function sendTelemetry(payload, serverUrl = exports.TELEMETRY_URL) {
|
|
66
98
|
try {
|
|
67
99
|
await fetch(serverUrl, {
|
|
68
100
|
method: "POST",
|
|
@@ -75,3 +107,32 @@ async function sendTelemetry(payload, serverUrl = TELEMETRY_URL) {
|
|
|
75
107
|
// Silently ignore — telemetry must never affect the nightly result
|
|
76
108
|
}
|
|
77
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.2",
|
|
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": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"vitest": "^3.0.0"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
|
-
"node": ">=
|
|
52
|
+
"node": ">=20"
|
|
53
53
|
},
|
|
54
54
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
55
55
|
"homepage": "https://hicortex.gamaze.com",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@huggingface/transformers": "^3.0.0",
|
|
67
67
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
68
|
-
"better-sqlite3": "^11.
|
|
68
|
+
"better-sqlite3": "^12.11.1",
|
|
69
69
|
"express": "^4.21.0",
|
|
70
70
|
"sqlite-vec": "^0.1.7"
|
|
71
71
|
}
|