@gamaze/hicortex 0.18.1 → 0.18.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/assets/dashboard.html +8 -3
- package/assets/viz.html +9 -3
- package/dist/consolidate.js +7 -7
- package/dist/dashboard.js +3 -3
- package/dist/db.js +24 -1
- package/dist/distiller.d.ts +9 -7
- package/dist/distiller.js +37 -19
- package/dist/eval/recall-sweep.js +2 -2
- package/dist/eval/reflection-census.js +3 -3
- package/dist/hosted-boot.d.ts +61 -0
- package/dist/hosted-boot.js +72 -0
- package/dist/index.js +13 -13
- package/dist/init.d.ts +15 -0
- package/dist/init.js +99 -8
- package/dist/learnings-identity.js +4 -4
- package/dist/localhost-bypass.d.ts +27 -0
- package/dist/localhost-bypass.js +71 -0
- package/dist/mcp-server.js +115 -19
- package/dist/prompts.js +12 -12
- package/dist/recall-index.js +7 -2
- package/dist/retrieval.js +2 -1
- package/dist/seed-lesson.js +1 -1
- package/dist/status.d.ts +8 -0
- package/dist/status.js +15 -1
- package/dist/storage.js +4 -4
- package/dist/token-budget.d.ts +34 -0
- package/dist/token-budget.js +131 -0
- package/dist/type-classify.d.ts +29 -26
- package/dist/type-classify.js +52 -45
- package/dist/type-labels.d.ts +48 -17
- package/dist/type-labels.js +89 -18
- package/dist/types.d.ts +10 -1
- package/dist/viz.d.ts +9 -1
- package/dist/viz.js +11 -2
- package/hermes-plugin/hicortex/client.py +1 -1
- package/hermes-plugin/hicortex/provider.py +13 -13
- package/package.json +1 -1
package/dist/init.js
CHANGED
|
@@ -34,6 +34,7 @@ exports.writeClientConfig = writeClientConfig;
|
|
|
34
34
|
exports.scaffoldDefaultDomains = scaffoldDefaultDomains;
|
|
35
35
|
exports.getPackageSpec = getPackageSpec;
|
|
36
36
|
exports.isEphemeralNpxPath = isEphemeralNpxPath;
|
|
37
|
+
exports.buildSupervisorPath = buildSupervisorPath;
|
|
37
38
|
exports.installSessionStartHook = installSessionStartHook;
|
|
38
39
|
exports.installRecallHooks = installRecallHooks;
|
|
39
40
|
exports.runInit = runInit;
|
|
@@ -45,6 +46,7 @@ exports.formatOnCalendarLines = formatOnCalendarLines;
|
|
|
45
46
|
exports.formatLaunchdIntervals = formatLaunchdIntervals;
|
|
46
47
|
exports.formatSystemdTimerBody = formatSystemdTimerBody;
|
|
47
48
|
const paths_js_1 = require("./paths.js");
|
|
49
|
+
const localhost_bypass_js_1 = require("./localhost-bypass.js");
|
|
48
50
|
const telemetry_js_1 = require("./telemetry.js");
|
|
49
51
|
const node_fs_1 = require("node:fs");
|
|
50
52
|
const node_path_1 = require("node:path");
|
|
@@ -1134,6 +1136,10 @@ function getPackageSpec(configDir = HICORTEX_HOME) {
|
|
|
1134
1136
|
function installDaemon() {
|
|
1135
1137
|
const os = (0, node_os_1.platform)();
|
|
1136
1138
|
const binaryArgs = resolveBinaryArgs();
|
|
1139
|
+
// #276: verify the supervisor can actually run (node resolvable on the
|
|
1140
|
+
// generated PATH) before writing the plist/unit — turns a silent DOA into a
|
|
1141
|
+
// loud install-time warning.
|
|
1142
|
+
verifySupervisorRuntime(binaryArgs);
|
|
1137
1143
|
if (os === "darwin") {
|
|
1138
1144
|
return installLaunchd(binaryArgs);
|
|
1139
1145
|
}
|
|
@@ -1185,6 +1191,71 @@ function resolveBinaryArgs() {
|
|
|
1185
1191
|
const packageSpec = getPackageSpec();
|
|
1186
1192
|
return [npxPath, "-y", packageSpec];
|
|
1187
1193
|
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Build the PATH the launchd/systemd supervisors receive (#276). Order:
|
|
1196
|
+
* 1. the binary's own dir — so a SIBLING node wins for nvm/volta/npm-global
|
|
1197
|
+
* installs (the version the global was installed under);
|
|
1198
|
+
* 2. the dir of the node the supervisor should run under — resolved via
|
|
1199
|
+
* `which node` (the symlink path, stable across upgrades); see
|
|
1200
|
+
* resolveNodeDir(). This is the generic rescue: for bun/pnpm/yarn globals
|
|
1201
|
+
* the bin dir has NO node sibling, and on Apple Silicon node lives in
|
|
1202
|
+
* /opt/homebrew/bin. Baking the resolved node dir in fixes every package
|
|
1203
|
+
* manager without enumerating them;
|
|
1204
|
+
* 3. the standard locations — including /opt/homebrew/bin (Apple Silicon
|
|
1205
|
+
* homebrew) as a belt-and-suspenders fallback for the no-sibling case.
|
|
1206
|
+
* Deduped (preserving first-seen order); empties dropped.
|
|
1207
|
+
*/
|
|
1208
|
+
function buildSupervisorPath(binaryArgs) {
|
|
1209
|
+
const binDir = (0, node_path_1.dirname)(binaryArgs[0]);
|
|
1210
|
+
const nodeDir = resolveNodeDir();
|
|
1211
|
+
return [binDir, nodeDir, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"]
|
|
1212
|
+
.filter((d, i, a) => d && a.indexOf(d) === i)
|
|
1213
|
+
.join(":");
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Resolve the dir of the node the supervisor should use (#276). Prefers
|
|
1217
|
+
* `which node` — the SYMLINK path, stable across version upgrades (homebrew
|
|
1218
|
+
* rotates the Cellar target but keeps /opt/homebrew/bin/node) — over
|
|
1219
|
+
* process.execPath, which on macOS is the resolved realpath (the versioned
|
|
1220
|
+
* Cellar dir, e.g. /opt/homebrew/Cellar/node/X.Y.Z/bin) and STALES on a
|
|
1221
|
+
* `brew upgrade node`, re-introducing the silent-death the fix targets. Falls
|
|
1222
|
+
* back to process.execPath's dir only if `which node` is unavailable.
|
|
1223
|
+
*/
|
|
1224
|
+
function resolveNodeDir() {
|
|
1225
|
+
try {
|
|
1226
|
+
const which = (0, node_child_process_1.execSync)("which node", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
1227
|
+
if (which)
|
|
1228
|
+
return (0, node_path_1.dirname)(which);
|
|
1229
|
+
}
|
|
1230
|
+
catch { /* node not on PATH — fall through to execPath */ }
|
|
1231
|
+
return (0, node_path_1.dirname)(process.execPath);
|
|
1232
|
+
}
|
|
1233
|
+
/** Dedup flag so the supervisor-runtime warning prints once per `init` run. */
|
|
1234
|
+
let supervisorRuntimeWarned = false;
|
|
1235
|
+
/**
|
|
1236
|
+
* Install-time smoke test (#276): spawn the resolved binary with the SAME PATH
|
|
1237
|
+
* the supervisor will use and confirm it can run (`--version`). Turns the
|
|
1238
|
+
* silent-dead-on-arrival case (node unresolvable under launchd's empty PATH →
|
|
1239
|
+
* the agent dies at the `#!/usr/bin/env node` shebang with exit 127, capture
|
|
1240
|
+
* stops silently, no signal in `status` because the shell PATH masks it) into a
|
|
1241
|
+
* LOUD install-time warning. Does NOT block install — the plist/unit is still
|
|
1242
|
+
* written so a PATH fix + reload recovers it without re-init.
|
|
1243
|
+
*/
|
|
1244
|
+
function verifySupervisorRuntime(binaryArgs) {
|
|
1245
|
+
if (supervisorRuntimeWarned)
|
|
1246
|
+
return;
|
|
1247
|
+
const supervisorEnv = { ...process.env, PATH: buildSupervisorPath(binaryArgs) };
|
|
1248
|
+
try {
|
|
1249
|
+
(0, node_child_process_1.execSync)([...binaryArgs, "--version"].join(" "), { stdio: "pipe", env: supervisorEnv });
|
|
1250
|
+
}
|
|
1251
|
+
catch {
|
|
1252
|
+
supervisorRuntimeWarned = true;
|
|
1253
|
+
console.error(" ⚠ WARNING: the scheduled daemon/nightly could not run with the generated PATH — " +
|
|
1254
|
+
"`node` was not found, so the supervisor will fail silently at runtime (capture stops). " +
|
|
1255
|
+
"Reinstall via `npm install -g @gamaze/hicortex` (recommended) or ensure node is at a " +
|
|
1256
|
+
"standard location, then re-run `npx @gamaze/hicortex init`.");
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1188
1259
|
/**
|
|
1189
1260
|
* Install (or verify) the CC SessionStart hook that runs the canonical command
|
|
1190
1261
|
* `hicortex learnings-identity` (aliased as the legacy `lessons-context`,
|
|
@@ -1305,7 +1376,7 @@ function installLaunchd(binaryArgs) {
|
|
|
1305
1376
|
// PATH must start with the binary's own directory so the sibling node
|
|
1306
1377
|
// binary (correct version for nvm installs) is found first.
|
|
1307
1378
|
// launchd has no PATH by default; without this, node itself won't be found.
|
|
1308
|
-
const
|
|
1379
|
+
const supervisorPath = buildSupervisorPath(binaryArgs);
|
|
1309
1380
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1310
1381
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1311
1382
|
<plist version="1.0">
|
|
@@ -1327,7 +1398,7 @@ ${programArgs}
|
|
|
1327
1398
|
<key>EnvironmentVariables</key>
|
|
1328
1399
|
<dict>
|
|
1329
1400
|
<key>PATH</key>
|
|
1330
|
-
<string>${
|
|
1401
|
+
<string>${supervisorPath}</string>
|
|
1331
1402
|
</dict>
|
|
1332
1403
|
</dict>
|
|
1333
1404
|
</plist>`;
|
|
@@ -1354,7 +1425,7 @@ function installSystemd(binaryArgs) {
|
|
|
1354
1425
|
const servicePath = (0, node_path_1.join)(unitDir, "hicortex.service");
|
|
1355
1426
|
const execStart = [...binaryArgs, "server"].join(" ");
|
|
1356
1427
|
// PATH must start with the binary's own directory (see installLaunchd for rationale).
|
|
1357
|
-
const
|
|
1428
|
+
const supervisorPath = buildSupervisorPath(binaryArgs);
|
|
1358
1429
|
const service = `[Unit]
|
|
1359
1430
|
Description=Hicortex MCP server — long-term memory for AI agents
|
|
1360
1431
|
|
|
@@ -1365,7 +1436,7 @@ Restart=on-failure
|
|
|
1365
1436
|
RestartSec=10
|
|
1366
1437
|
StandardOutput=journal
|
|
1367
1438
|
StandardError=journal
|
|
1368
|
-
Environment=PATH=${
|
|
1439
|
+
Environment=PATH=${supervisorPath}
|
|
1369
1440
|
|
|
1370
1441
|
[Install]
|
|
1371
1442
|
WantedBy=default.target
|
|
@@ -1404,6 +1475,13 @@ async function runInit(options = {}) {
|
|
|
1404
1475
|
// first — every writer downstream loads through loadConfigStrict.
|
|
1405
1476
|
if (options.repairConfig) {
|
|
1406
1477
|
quarantineMalformedConfig((0, node_path_1.join)(HICORTEX_HOME, "config.json"));
|
|
1478
|
+
// CR warning 2 (#271): repair-config is a plausible post-upgrade recovery
|
|
1479
|
+
// action, so it MUST (re)write the localhost-bypass marker itself — defensive
|
|
1480
|
+
// against a future early-return in this block. The full-init path writes it
|
|
1481
|
+
// again at line ~1615 (idempotent: same content, returns false the second
|
|
1482
|
+
// time). Never written in hosted mode (the boot assertion refuses to start
|
|
1483
|
+
// with the marker present).
|
|
1484
|
+
(0, localhost_bypass_js_1.writeLocalhostBypassMarker)(HICORTEX_HOME);
|
|
1407
1485
|
}
|
|
1408
1486
|
if (options.serverUrl) {
|
|
1409
1487
|
await runClientInit(options.serverUrl, options.agentName);
|
|
@@ -1500,6 +1578,16 @@ async function runInit(options = {}) {
|
|
|
1500
1578
|
// Classification activates automatically once an LLM is configured; until
|
|
1501
1579
|
// then domains sit inert (strict-skip path).
|
|
1502
1580
|
scaffoldDefaultDomains(configPath);
|
|
1581
|
+
// Write the localhost auth-bypass marker (#110 §2, #271 — Phase 0B). The
|
|
1582
|
+
// bypass is marker-gated from 0.18: self-hosted init writes the marker so
|
|
1583
|
+
// existing installs keep the bypass after upgrade + re-init; a hosted tenant
|
|
1584
|
+
// dir is fail-closed by default. Idempotent (overwrites an existing marker,
|
|
1585
|
+
// refreshing the note). Never written in hosted mode (the boot assertion
|
|
1586
|
+
// would refuse to start with the marker present).
|
|
1587
|
+
const markerCreated = (0, localhost_bypass_js_1.writeLocalhostBypassMarker)(HICORTEX_HOME);
|
|
1588
|
+
if (markerCreated) {
|
|
1589
|
+
console.log(" ✓ Localhost auth-bypass marker written");
|
|
1590
|
+
}
|
|
1503
1591
|
// Per-agent identity id (#179): server mode writes it ONLY when the operator
|
|
1504
1592
|
// passes --agent-name. Without the flag no agentName is written and the
|
|
1505
1593
|
// co-located CC shares the global identity (global by default). Explicit flag
|
|
@@ -1944,9 +2032,12 @@ function formatSystemdTimerBody(isInterval, intervalSec, hours, jitterSec) {
|
|
|
1944
2032
|
*/
|
|
1945
2033
|
function writeScheduleUnit(opts) {
|
|
1946
2034
|
const binaryArgs = resolveBinaryArgs();
|
|
2035
|
+
// #276: verify the scheduled nightly/capture can run before writing its unit.
|
|
2036
|
+
verifySupervisorRuntime(binaryArgs);
|
|
1947
2037
|
const os = (0, node_os_1.platform)();
|
|
1948
|
-
// PATH
|
|
1949
|
-
|
|
2038
|
+
// PATH the supervisor receives — includes the dir of the node running init
|
|
2039
|
+
// (process.execPath) so bun/pnpm/yarn globals resolve node under launchd (#276).
|
|
2040
|
+
const supervisorPath = buildSupervisorPath(binaryArgs);
|
|
1950
2041
|
// One canonical nightly log path across platforms — status output, docs,
|
|
1951
2042
|
// and support instructions all reference this single location.
|
|
1952
2043
|
const logPath = (0, node_path_1.join)(HICORTEX_HOME, "nightly.log");
|
|
@@ -1999,7 +2090,7 @@ ${scheduleBlock}
|
|
|
1999
2090
|
<key>EnvironmentVariables</key>
|
|
2000
2091
|
<dict>
|
|
2001
2092
|
<key>PATH</key>
|
|
2002
|
-
<string>${
|
|
2093
|
+
<string>${supervisorPath}</string>
|
|
2003
2094
|
</dict>
|
|
2004
2095
|
</dict>
|
|
2005
2096
|
</plist>`;
|
|
@@ -2033,7 +2124,7 @@ Type=oneshot
|
|
|
2033
2124
|
ExecStart=${execStart}
|
|
2034
2125
|
${opts.timeoutMin ? `TimeoutStartSec=${opts.timeoutMin}min\n` : ""}StandardOutput=append:${logPath}
|
|
2035
2126
|
StandardError=append:${logPath}
|
|
2036
|
-
Environment=PATH=${
|
|
2127
|
+
Environment=PATH=${supervisorPath}
|
|
2037
2128
|
Environment=HOME=${(0, node_os_1.homedir)()}
|
|
2038
2129
|
WorkingDirectory=${(0, node_os_1.homedir)()}`;
|
|
2039
2130
|
// Timer body: OnUnitActiveSec (interval, watchdog) or one OnCalendar line
|
|
@@ -117,7 +117,7 @@ async function fetchLessonsBlock(cfg) {
|
|
|
117
117
|
parts.push("BEFORE making decisions, search memory: `hicortex_search` for prior decisions on the same topic.");
|
|
118
118
|
parts.push("Use `hicortex_recent` at session start for recent project state.");
|
|
119
119
|
if (lessonLines.length > 0) {
|
|
120
|
-
parts.push("", "###
|
|
120
|
+
parts.push("", "### Learnings (updated nightly)");
|
|
121
121
|
parts.push(...lessonLines);
|
|
122
122
|
}
|
|
123
123
|
const { index } = data;
|
|
@@ -125,16 +125,16 @@ async function fetchLessonsBlock(cfg) {
|
|
|
125
125
|
parts.push("", "### Memory Index");
|
|
126
126
|
for (const domain of moduleIndex.domains) {
|
|
127
127
|
const kwStr = domain.keywords.length > 0 ? `: ${domain.keywords.join(", ")}` : "";
|
|
128
|
-
parts.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount}
|
|
128
|
+
parts.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount} Learnings)${kwStr}`);
|
|
129
129
|
if (domain.projects.length > 0)
|
|
130
130
|
parts.push(` ${domain.projects.join(" | ")}`);
|
|
131
131
|
}
|
|
132
|
-
parts.push(`${index.total} memories, ${index.lessonCount}
|
|
132
|
+
parts.push(`${index.total} memories, ${index.lessonCount} Learnings, ${index.sourceCount} agents. Search with \`hicortex_search\`.`);
|
|
133
133
|
}
|
|
134
134
|
else if (index.projects.length > 0) {
|
|
135
135
|
parts.push("", "### Memory Index");
|
|
136
136
|
parts.push(index.projects.map(p => `${p.name}: ${p.count}`).join(" | "));
|
|
137
|
-
parts.push(`${index.total} memories, ${index.lessonCount}
|
|
137
|
+
parts.push(`${index.total} memories, ${index.lessonCount} Learnings, ${index.sourceCount} agents. Search with \`hicortex_search\`.`);
|
|
138
138
|
}
|
|
139
139
|
return parts.join("\n");
|
|
140
140
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Marker filename inside the Hicortex home dir. */
|
|
2
|
+
export declare const LOCALHOST_BYPASS_MARKER = ".allow-localhost-bypass";
|
|
3
|
+
/** Marker contents — a one-line note. Its mere PRESENCE is the signal. */
|
|
4
|
+
export declare const LOCALHOST_BYPASS_MARKER_CONTENT: string;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the marker file path for a given home dir. Defaults to the canonical
|
|
7
|
+
* Hicortex home (honors HICORTEX_HOME), so callers in tests can point the env
|
|
8
|
+
* override at a temp dir.
|
|
9
|
+
*/
|
|
10
|
+
export declare function localhostBypassMarkerPath(home?: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Does the localhost auth-bypass marker exist? Pure filesystem check — no
|
|
13
|
+
* logging, no side-effects. Used by both createAuthMiddleware (gates the
|
|
14
|
+
* bypass per-request via a boot-time capture in mcp-server.ts) and the
|
|
15
|
+
* hosted-mode boot assertion.
|
|
16
|
+
*/
|
|
17
|
+
export declare function localhostBypassEnabled(home?: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Write the localhost auth-bypass marker file (self-hosted init only — never
|
|
20
|
+
* in hosted mode). Idempotent: overwrites an existing marker so a re-init
|
|
21
|
+
* refreshes the explanatory note. Ensures the parent dir exists. Does NOT
|
|
22
|
+
* touch auth or any other config — just the one marker file.
|
|
23
|
+
*
|
|
24
|
+
* Returns true when a NEW marker was created (for init's "✓" reporting), false
|
|
25
|
+
* when one already existed (refreshed in place).
|
|
26
|
+
*/
|
|
27
|
+
export declare function writeLocalhostBypassMarker(home?: string): boolean;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LOCALHOST_BYPASS_MARKER_CONTENT = exports.LOCALHOST_BYPASS_MARKER = void 0;
|
|
4
|
+
exports.localhostBypassMarkerPath = localhostBypassMarkerPath;
|
|
5
|
+
exports.localhostBypassEnabled = localhostBypassEnabled;
|
|
6
|
+
exports.writeLocalhostBypassMarker = writeLocalhostBypassMarker;
|
|
7
|
+
/**
|
|
8
|
+
* Localhost auth-bypass marker file (#110 §2, #271 — Phase 0B).
|
|
9
|
+
*
|
|
10
|
+
* The localhost auth bypass in createAuthMiddleware (viz.ts) is marker-GATED
|
|
11
|
+
* from 0.18: it applies ONLY when this marker file exists in the Hicortex
|
|
12
|
+
* home dir. Self-hosted `init` writes the marker, so existing installs keep
|
|
13
|
+
* working after upgrade + re-init; a hosted tenant dir provisioned by any
|
|
14
|
+
* means (script, hand, restored tar) is fail-closed by default — no marker,
|
|
15
|
+
* no bypass, every connection (localhost included) needs the bearer token.
|
|
16
|
+
*
|
|
17
|
+
* Rationale (spec 2026-07-27 §2): with the bypass unconditional, a future
|
|
18
|
+
* `trust proxy` enablement would make `req.ip` header-spoofable and the
|
|
19
|
+
* bypass remotely triggerable. Inverting the default to "off unless marked"
|
|
20
|
+
* makes the bypass opt-in via a filesystem side-effect of self-hosted init,
|
|
21
|
+
* so a tenant home built from a bare config + DB restore cannot accidentally
|
|
22
|
+
* ship with the bypass active. The hosted-mode boot assertion (mcp-server.ts)
|
|
23
|
+
* refuses to start if BOTH hostedMode=true AND the marker is present, so even
|
|
24
|
+
* a stray marker cannot open a hosted tenant.
|
|
25
|
+
*
|
|
26
|
+
* Marker file name: `.allow-localhost-bypass` (dot-prefixed; not a secret —
|
|
27
|
+
* its mere presence is the signal; no contents needed).
|
|
28
|
+
*/
|
|
29
|
+
const node_fs_1 = require("node:fs");
|
|
30
|
+
const node_path_1 = require("node:path");
|
|
31
|
+
const paths_js_1 = require("./paths.js");
|
|
32
|
+
/** Marker filename inside the Hicortex home dir. */
|
|
33
|
+
exports.LOCALHOST_BYPASS_MARKER = ".allow-localhost-bypass";
|
|
34
|
+
/** Marker contents — a one-line note. Its mere PRESENCE is the signal. */
|
|
35
|
+
exports.LOCALHOST_BYPASS_MARKER_CONTENT = "# Written by `hicortex init` (self-hosted). Opt-in to the localhost auth\n" +
|
|
36
|
+
"# bypass. DELETE this file to require the bearer token on localhost too\n" +
|
|
37
|
+
"# (fail-closed). Hosted-mode (hostedMode:true) refuses to start with this\n" +
|
|
38
|
+
"# marker present — see specs/2026-07-27-hosted-service.md §2.\n";
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the marker file path for a given home dir. Defaults to the canonical
|
|
41
|
+
* Hicortex home (honors HICORTEX_HOME), so callers in tests can point the env
|
|
42
|
+
* override at a temp dir.
|
|
43
|
+
*/
|
|
44
|
+
function localhostBypassMarkerPath(home = (0, paths_js_1.hicortexHome)()) {
|
|
45
|
+
return (0, node_path_1.join)(home, exports.LOCALHOST_BYPASS_MARKER);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Does the localhost auth-bypass marker exist? Pure filesystem check — no
|
|
49
|
+
* logging, no side-effects. Used by both createAuthMiddleware (gates the
|
|
50
|
+
* bypass per-request via a boot-time capture in mcp-server.ts) and the
|
|
51
|
+
* hosted-mode boot assertion.
|
|
52
|
+
*/
|
|
53
|
+
function localhostBypassEnabled(home = (0, paths_js_1.hicortexHome)()) {
|
|
54
|
+
return (0, node_fs_1.existsSync)(localhostBypassMarkerPath(home));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Write the localhost auth-bypass marker file (self-hosted init only — never
|
|
58
|
+
* in hosted mode). Idempotent: overwrites an existing marker so a re-init
|
|
59
|
+
* refreshes the explanatory note. Ensures the parent dir exists. Does NOT
|
|
60
|
+
* touch auth or any other config — just the one marker file.
|
|
61
|
+
*
|
|
62
|
+
* Returns true when a NEW marker was created (for init's "✓" reporting), false
|
|
63
|
+
* when one already existed (refreshed in place).
|
|
64
|
+
*/
|
|
65
|
+
function writeLocalhostBypassMarker(home = (0, paths_js_1.hicortexHome)()) {
|
|
66
|
+
const markerPath = localhostBypassMarkerPath(home);
|
|
67
|
+
const existed = (0, node_fs_1.existsSync)(markerPath);
|
|
68
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(markerPath), { recursive: true });
|
|
69
|
+
(0, node_fs_1.writeFileSync)(markerPath, exports.LOCALHOST_BYPASS_MARKER_CONTENT, { mode: 0o644 });
|
|
70
|
+
return !existed;
|
|
71
|
+
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -59,6 +59,10 @@ const db_js_1 = require("./db.js");
|
|
|
59
59
|
const llm_js_1 = require("./llm.js");
|
|
60
60
|
const features_js_1 = require("./features.js");
|
|
61
61
|
const config_read_js_1 = require("./config-read.js");
|
|
62
|
+
const localhost_bypass_js_1 = require("./localhost-bypass.js");
|
|
63
|
+
const hosted_boot_js_1 = require("./hosted-boot.js");
|
|
64
|
+
const token_budget_js_1 = require("./token-budget.js");
|
|
65
|
+
const paths_js_1 = require("./paths.js");
|
|
62
66
|
const state_js_1 = require("./state.js");
|
|
63
67
|
const embedder_js_1 = require("./embedder.js");
|
|
64
68
|
const storage = __importStar(require("./storage.js"));
|
|
@@ -183,10 +187,10 @@ function createMcpServer() {
|
|
|
183
187
|
}
|
|
184
188
|
});
|
|
185
189
|
// -- hicortex_ingest --
|
|
186
|
-
server.tool("hicortex_ingest", "Store a new memory in long-term storage. Use for
|
|
190
|
+
server.tool("hicortex_ingest", "Store a new memory in long-term storage. Use for Knowledge, Decisions, or Learnings.", {
|
|
187
191
|
content: zod_1.z.string().describe("Memory content to store"),
|
|
188
192
|
project: zod_1.z.string().optional().describe("Project this memory belongs to"),
|
|
189
|
-
memory_type: zod_1.z.enum(["
|
|
193
|
+
memory_type: zod_1.z.enum(["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"]).optional().describe("Type of memory (default: Experience). Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term)."),
|
|
190
194
|
}, async ({ content, project, memory_type }) => {
|
|
191
195
|
if (!db)
|
|
192
196
|
return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
|
|
@@ -195,7 +199,8 @@ function createMcpServer() {
|
|
|
195
199
|
const id = storage.insertMemory(db, content, embedding, {
|
|
196
200
|
sourceAgent: "claude-code/manual",
|
|
197
201
|
project,
|
|
198
|
-
|
|
202
|
+
// Normalize legacy raw enum to the canonical term the DB stores.
|
|
203
|
+
memoryType: memory_type ? (0, type_labels_js_1.normalizeMemoryType)(memory_type) : "experience",
|
|
199
204
|
});
|
|
200
205
|
return { content: [{ type: "text", text: `Memory stored (id: ${id.slice(0, 8)})` }] };
|
|
201
206
|
}
|
|
@@ -208,7 +213,7 @@ function createMcpServer() {
|
|
|
208
213
|
id: zod_1.z.string().describe("Memory ID (from search results, first 8 chars or full UUID)"),
|
|
209
214
|
content: zod_1.z.string().optional().describe("New content text"),
|
|
210
215
|
project: zod_1.z.string().optional().describe("New project name"),
|
|
211
|
-
memory_type: zod_1.z.enum(["
|
|
216
|
+
memory_type: zod_1.z.enum(["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"]).optional().describe("New memory type. Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term)."),
|
|
212
217
|
}, async ({ id, content, project, memory_type }) => {
|
|
213
218
|
if (!db)
|
|
214
219
|
return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
|
|
@@ -222,8 +227,9 @@ function createMcpServer() {
|
|
|
222
227
|
fields.content = content;
|
|
223
228
|
if (project !== undefined)
|
|
224
229
|
fields.project = project;
|
|
230
|
+
// Normalize legacy raw enum to canonical human terms before DB write.
|
|
225
231
|
if (memory_type !== undefined)
|
|
226
|
-
fields.memory_type = memory_type;
|
|
232
|
+
fields.memory_type = (0, type_labels_js_1.normalizeMemoryType)(memory_type);
|
|
227
233
|
if (Object.keys(fields).length === 0) {
|
|
228
234
|
return { content: [{ type: "text", text: "No fields to update" }], isError: true };
|
|
229
235
|
}
|
|
@@ -319,7 +325,7 @@ function createMcpServer() {
|
|
|
319
325
|
const moduleIndex = state.moduleIndex;
|
|
320
326
|
if (moduleIndex && moduleIndex.domains.length > 0) {
|
|
321
327
|
const text = moduleIndex.domains.map((d) => {
|
|
322
|
-
const head = `**${d.name}** (${d.memoryCount} memories, ${d.lessonCount}
|
|
328
|
+
const head = `**${d.name}** (${d.memoryCount} memories, ${d.lessonCount} Learnings)`;
|
|
323
329
|
// Content-based domains carry a description and no projects; legacy
|
|
324
330
|
// project-grouping domains carry a project list + keywords.
|
|
325
331
|
if (d.description && d.projects.length === 0) {
|
|
@@ -404,6 +410,59 @@ function createMcpServer() {
|
|
|
404
410
|
async function startServer(options = {}) {
|
|
405
411
|
const port = options.port ?? 8787;
|
|
406
412
|
const host = options.host ?? "0.0.0.0";
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
// Hosted-mode boot gate (#110 §1-§2, #271 — Phase 0B).
|
|
415
|
+
//
|
|
416
|
+
// MUST run BEFORE resolveDbPath/initDb: in hosted mode with HICORTEX_DB_PATH
|
|
417
|
+
// set, the server must refuse the attacker-chosen DB location WITHOUT first
|
|
418
|
+
// touching it. The hosted signals (hostedMode from config, bypassMarkerPresent
|
|
419
|
+
// from the marker file) do NOT depend on the DB, so reading them now is safe.
|
|
420
|
+
// CR warning 3: this block was previously after initDb, letting a hostile
|
|
421
|
+
// HICORTEX_DB_PATH create/touch a file at the chosen path before the gate.
|
|
422
|
+
//
|
|
423
|
+
// CR warning 1: the marker is a HOME-level file (like config.json, written by
|
|
424
|
+
// init to HICORTEX_HOME). Read it from hicortexHome() — NOT stateDir, which
|
|
425
|
+
// is dirname(dbPath) and drifts when HICORTEX_DB_PATH relocates the DB. The
|
|
426
|
+
// config key hostedMode likewise lives at <hicortexHome>/config.json.
|
|
427
|
+
//
|
|
428
|
+
// Decision logic lives in hosted-boot.ts (pure, unit-tested); the side-effect
|
|
429
|
+
// (console.error + process.exit) is local to boot. The marker state is
|
|
430
|
+
// captured once here and reused below to gate the localhost bypass in
|
|
431
|
+
// createAuthMiddleware (no per-request stat). CR warning 4: the upgrade-path
|
|
432
|
+
// warning is decided by the pure shouldEmitBypassWarning helper (behavior-
|
|
433
|
+
// tested), not an inline branch.
|
|
434
|
+
const bootConfig = readConfigFile((0, paths_js_1.hicortexHome)());
|
|
435
|
+
const hostedMode = (0, config_read_js_1.readStrictBoolean)(bootConfig ?? {}, "hostedMode") === true;
|
|
436
|
+
let bypassMarkerPresent = (0, localhost_bypass_js_1.localhostBypassEnabled)();
|
|
437
|
+
const bootDecision = (0, hosted_boot_js_1.checkHostedBoot)({
|
|
438
|
+
hostedMode,
|
|
439
|
+
dbPathEnvSet: !!process.env.HICORTEX_DB_PATH,
|
|
440
|
+
bypassMarkerPresent,
|
|
441
|
+
});
|
|
442
|
+
if (!bootDecision.ok) {
|
|
443
|
+
console.error(bootDecision.message);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
// Upgrade migration (CR S1): self-hosted server-mode CC MCP registration
|
|
447
|
+
// carries NO bearer token (init.ts:192 — only client-mode adds the header),
|
|
448
|
+
// so it relies entirely on the localhost bypass. An existing install that
|
|
449
|
+
// upgrades without re-running init has no marker → the bypass silently
|
|
450
|
+
// disappears → every server-mode CC MCP call 401s. Auto-write the marker on
|
|
451
|
+
// first post-upgrade boot in self-hosted mode to preserve the prior
|
|
452
|
+
// unconditional-bypass behaviour. Hosted mode is untouched: checkHostedBoot
|
|
453
|
+
// refuses to start with a marker present, so this block — gated on
|
|
454
|
+
// !hostedMode — never runs for a hosted tenant. bypassMarkerPresent is
|
|
455
|
+
// reassigned so createAuthMiddleware below gates the bypass for THIS boot
|
|
456
|
+
// too (the file write and the in-memory flag stay in sync).
|
|
457
|
+
if (!hostedMode && !bypassMarkerPresent) {
|
|
458
|
+
(0, localhost_bypass_js_1.writeLocalhostBypassMarker)((0, paths_js_1.hicortexHome)());
|
|
459
|
+
bypassMarkerPresent = true;
|
|
460
|
+
console.log("[hicortex] Localhost auth-bypass marker written (upgrade migration).");
|
|
461
|
+
}
|
|
462
|
+
const bypassWarning = (0, hosted_boot_js_1.shouldEmitBypassWarning)(hostedMode, bypassMarkerPresent);
|
|
463
|
+
if (bypassWarning) {
|
|
464
|
+
console.warn(bypassWarning);
|
|
465
|
+
}
|
|
407
466
|
// Initialize core
|
|
408
467
|
const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
|
|
409
468
|
console.log(`[hicortex] Initializing database at ${dbPath}`);
|
|
@@ -429,6 +488,11 @@ async function startServer(options = {}) {
|
|
|
429
488
|
const { agentId } = (0, init_js_1.ensureAndPersistAgentId)((0, node_path_1.join)(stateDir, "config.json"));
|
|
430
489
|
savedConfig.agentId = agentId;
|
|
431
490
|
}
|
|
491
|
+
// #5: token-budget enforcement. Mode-agnostic — gates on cap > 0. Self-hosted
|
|
492
|
+
// uses config llmTokensPerMonth (default 0 = off); hosted uses HICORTEX_TOKEN_CAP
|
|
493
|
+
// env (provider-set, tenant-immutable) which takes precedence. Initialised here
|
|
494
|
+
// (after stateDir + savedConfig are known) so the warn-dedup can seed from state.
|
|
495
|
+
(0, token_budget_js_1.initTokenBudget)(stateDir, savedConfig?.llmTokensPerMonth);
|
|
432
496
|
if (savedConfig?.llmBackend === "claude-cli") {
|
|
433
497
|
const claudePath = (0, llm_js_1.findClaudeBinary)();
|
|
434
498
|
if (claudePath) {
|
|
@@ -613,7 +677,7 @@ async function startServer(options = {}) {
|
|
|
613
677
|
// /dashboard has its own shell-exemption pattern. Gives the console one entry
|
|
614
678
|
// point: http://<host>:8787/ → /dashboard.
|
|
615
679
|
app.get("/", (_req, res) => res.redirect("/dashboard"));
|
|
616
|
-
app.use((0, viz_js_1.createAuthMiddleware)(authToken, authTokenPrevious));
|
|
680
|
+
app.use((0, viz_js_1.createAuthMiddleware)(authToken, authTokenPrevious, bypassMarkerPresent));
|
|
617
681
|
// SSE transport management — each connection gets its own McpServer instance
|
|
618
682
|
const transports = new Map();
|
|
619
683
|
// Health endpoint — PUBLIC minimal probe. Unauthenticated (the auth
|
|
@@ -692,11 +756,15 @@ async function startServer(options = {}) {
|
|
|
692
756
|
res.status(400).json({ error: "Missing or invalid 'content' field" });
|
|
693
757
|
return;
|
|
694
758
|
}
|
|
695
|
-
const validTypes =
|
|
759
|
+
const validTypes = type_labels_js_1.ACCEPTED_MEMORY_TYPES;
|
|
696
760
|
if (memory_type && !validTypes.includes(memory_type)) {
|
|
697
761
|
res.status(400).json({ error: `Invalid memory_type: ${memory_type}` });
|
|
698
762
|
return;
|
|
699
763
|
}
|
|
764
|
+
// Normalize legacy raw enum (fact/episode/decision/lesson) to the
|
|
765
|
+
// canonical term the DB stores (knowledge/experience/decisions/learnings).
|
|
766
|
+
// Canonical values pass through unchanged.
|
|
767
|
+
const normalizedType = memory_type ? (0, type_labels_js_1.normalizeMemoryType)(memory_type) : memory_type;
|
|
700
768
|
// Dedup by source_session (idempotent — skip if already ingested)
|
|
701
769
|
if (source_session) {
|
|
702
770
|
const existing = db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE source_session = ?").get(source_session);
|
|
@@ -714,7 +782,7 @@ async function startServer(options = {}) {
|
|
|
714
782
|
sourceDomain: typeof source_domain === "string" ? source_domain : null,
|
|
715
783
|
sourceSession: source_session ?? undefined,
|
|
716
784
|
project: project ?? undefined,
|
|
717
|
-
memoryType:
|
|
785
|
+
memoryType: normalizedType ?? "experience",
|
|
718
786
|
// 0.16.x: privacy defaults to null (vestigial column). A legacy client
|
|
719
787
|
// that sends an explicit value is honored; absent → null.
|
|
720
788
|
privacy: typeof privacy === "string" ? privacy : null,
|
|
@@ -1021,12 +1089,27 @@ async function startServer(options = {}) {
|
|
|
1021
1089
|
const sourcePrefix = session_id
|
|
1022
1090
|
? `${session_id}${segment_id ? `#${segment_id}` : ""}`
|
|
1023
1091
|
: undefined;
|
|
1092
|
+
// #5: declared outside the try so the finally can record tokens spent even
|
|
1093
|
+
// when distillSession throws partway through (the LLM calls already happened).
|
|
1094
|
+
let distillUsage = { prompt: 0, completion: 0, total: 0 };
|
|
1024
1095
|
try {
|
|
1096
|
+
// #5: token-budget gate — refuse (429) BEFORE the LLM call if the tenant is
|
|
1097
|
+
// already at/over the monthly cap. Placed after the dedup short-circuits so
|
|
1098
|
+
// a skipped duplicate neither trips the gate nor consumes budget. The client
|
|
1099
|
+
// capture loop holds its cursor on 429 (dup-over-loss, capture.ts:303).
|
|
1100
|
+
if ((0, token_budget_js_1.isTokenBudgetExceeded)(stateDir)) {
|
|
1101
|
+
res.status(429).json({ error: "token budget exceeded", retry: "next billing period" });
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1025
1104
|
// Collect gate-dropped entries so they can ride back in the response and
|
|
1026
1105
|
// land in the caller's file-persisted nightly log (#156 audit trail); the
|
|
1027
1106
|
// server-side per-entry console.log in distillChunk stays as well.
|
|
1028
1107
|
const dropped = [];
|
|
1029
|
-
const entries = await (0, distiller_js_1.distillSession)(llm, conversationText, project ?? "unknown", date, chunkSize, dropped)
|
|
1108
|
+
const entries = await (0, distiller_js_1.distillSession)(llm, conversationText, project ?? "unknown", date, chunkSize, dropped, (u) => {
|
|
1109
|
+
distillUsage.prompt += u.prompt_tokens ?? 0;
|
|
1110
|
+
distillUsage.completion += u.completion_tokens ?? 0;
|
|
1111
|
+
distillUsage.total += u.total_tokens ?? 0;
|
|
1112
|
+
});
|
|
1030
1113
|
// Phase 1 — embed every chunk up front (async). If ANY embed fails we
|
|
1031
1114
|
// never reach the insert, so nothing is stored.
|
|
1032
1115
|
const createdAt = new Date(date).toISOString();
|
|
@@ -1061,9 +1144,9 @@ async function startServer(options = {}) {
|
|
|
1061
1144
|
sourceSession: sourcePrefix ? `${sourcePrefix}#${i}` : undefined,
|
|
1062
1145
|
project: project ?? undefined,
|
|
1063
1146
|
// #216: the distiller now classifies each entry as
|
|
1064
|
-
//
|
|
1147
|
+
// experience/knowledge/decisions via the [E]/[K]/[D] tag parsed in
|
|
1065
1148
|
// distiller.ts. Pre-#216 distiller output (no tag) defaults to
|
|
1066
|
-
//
|
|
1149
|
+
// experience in the parser, so this is backward compatible.
|
|
1067
1150
|
memoryType,
|
|
1068
1151
|
// 0.16.x: privacy defaults to null (vestigial column). A legacy
|
|
1069
1152
|
// client that sends an explicit value is honored; absent → null.
|
|
@@ -1084,6 +1167,14 @@ async function startServer(options = {}) {
|
|
|
1084
1167
|
res.status(500).json({ error: "Distillation failed" });
|
|
1085
1168
|
console.error(`[hicortex] /distill: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
1086
1169
|
}
|
|
1170
|
+
finally {
|
|
1171
|
+
// #5: record tokens spent against the monthly budget — in finally so a
|
|
1172
|
+
// mid-distil throw (some chunks' LLM calls already happened) still counts.
|
|
1173
|
+
// No-op when cap=0 (enforcement off) or distillUsage.total=0 (gate refused
|
|
1174
|
+
// / no chunk reached an LLM call).
|
|
1175
|
+
if (distillUsage.total > 0)
|
|
1176
|
+
(0, token_budget_js_1.recordDistillUsage)(stateDir, distillUsage);
|
|
1177
|
+
}
|
|
1087
1178
|
});
|
|
1088
1179
|
// -------------------------------------------------------------------------
|
|
1089
1180
|
// REST /update — update a memory (and re-embed when content changes).
|
|
@@ -1111,19 +1202,24 @@ async function startServer(options = {}) {
|
|
|
1111
1202
|
fields.content = content;
|
|
1112
1203
|
if (project !== undefined)
|
|
1113
1204
|
fields.project = project;
|
|
1114
|
-
if (memory_type !== undefined)
|
|
1115
|
-
fields.memory_type = memory_type;
|
|
1116
1205
|
if (privacy !== undefined)
|
|
1117
1206
|
fields.privacy = privacy;
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
}
|
|
1122
|
-
const validTypes = ["episode", "lesson", "fact", "decision"];
|
|
1207
|
+
// Validate + normalize memory_type BEFORE adding to `fields` so the
|
|
1208
|
+
// empty-fields check below correctly counts a memory_type-only update.
|
|
1209
|
+
const validTypes = type_labels_js_1.ACCEPTED_MEMORY_TYPES;
|
|
1123
1210
|
if (memory_type !== undefined && !validTypes.includes(memory_type)) {
|
|
1124
1211
|
res.status(400).json({ error: `Invalid memory_type: ${memory_type}` });
|
|
1125
1212
|
return;
|
|
1126
1213
|
}
|
|
1214
|
+
// Normalize legacy raw enum (fact/episode/decision/lesson) to the
|
|
1215
|
+
// canonical term the DB stores (knowledge/experience/decisions/learnings).
|
|
1216
|
+
// Canonical values pass through unchanged.
|
|
1217
|
+
if (memory_type !== undefined)
|
|
1218
|
+
fields.memory_type = (0, type_labels_js_1.normalizeMemoryType)(memory_type);
|
|
1219
|
+
if (Object.keys(fields).length === 0) {
|
|
1220
|
+
res.status(400).json({ error: "No fields to update" });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1127
1223
|
try {
|
|
1128
1224
|
storage.updateMemory(db, fullId, fields);
|
|
1129
1225
|
// Re-embed when content changes
|
package/dist/prompts.js
CHANGED
|
@@ -48,7 +48,7 @@ function reflection(memoriesBlock, recentLessons) {
|
|
|
48
48
|
Like human learning: we grow fastest when we reinforce what works AND correct what doesn't. A system that only learns from mistakes becomes overly cautious. A system that only learns from successes never improves. The combination multiplies.
|
|
49
49
|
|
|
50
50
|
GENERALITY BAR (read carefully — the most important rule):
|
|
51
|
-
Every lesson MUST be a generalizable operating principle that transfers across contexts, agents, and projects. It is NOT: an incident report, a changelog entry, a one-event fact, a tool-specific recipe, or a note about a named entity. If a memory is only interesting as "what happened today", it is an
|
|
51
|
+
Every lesson MUST be a generalizable operating principle that transfers across contexts, agents, and projects. It is NOT: an incident report, a changelog entry, a one-event fact, a tool-specific recipe, or a note about a named entity. If a memory is only interesting as "what happened today", it is an EXPERIENCE — do not emit a lesson for it. Abstract away specific tool names, hostnames, and incident details from the lesson text; state the transferable rule.
|
|
52
52
|
|
|
53
53
|
Quality over quantity. 1-3 lessons is typical. An empty array [] is the CORRECT response when memories show routine competent work without noteworthy patterns, surprises, or friction. Do not manufacture lessons from nothing.
|
|
54
54
|
|
|
@@ -121,8 +121,8 @@ EXTRACT into this markdown format:
|
|
|
121
121
|
### Decisions Made
|
|
122
122
|
- [D] [SUBJECT]: [decision] — [reasoning] (${date})
|
|
123
123
|
|
|
124
|
-
###
|
|
125
|
-
- [
|
|
124
|
+
### Knowledge Learned
|
|
125
|
+
- [K] [SUBJECT]: [knowledge] — [context/source] (${date})
|
|
126
126
|
|
|
127
127
|
### Problems & Solutions
|
|
128
128
|
- [E] [SUBJECT]: [problem] → [solution that worked] (${date})
|
|
@@ -131,7 +131,7 @@ EXTRACT into this markdown format:
|
|
|
131
131
|
- [D] [SUBJECT]: [what changed], [from → to] (${date})
|
|
132
132
|
|
|
133
133
|
### Key Entities & Relationships
|
|
134
|
-
- [
|
|
134
|
+
- [K] [entity A] → [relationship] → [entity B] (${date})
|
|
135
135
|
|
|
136
136
|
### Corrections & Rejections
|
|
137
137
|
- [E] [SUBJECT]: [what AI proposed] → [why rejected/corrected] → [what user wanted instead] (${date})
|
|
@@ -139,18 +139,18 @@ EXTRACT into this markdown format:
|
|
|
139
139
|
user corrections of AI assumptions, quality complaints like "too verbose")
|
|
140
140
|
|
|
141
141
|
TYPE TAG (critical — prefix EVERY bullet with exactly one letter + space):
|
|
142
|
-
- [E]
|
|
142
|
+
- [E] EXPERIENCE — a specific event, interaction, or narrative: "tried X, failed
|
|
143
143
|
because Y", a correction, a debugging session, a one-time occurrence. The
|
|
144
144
|
DEFAULT when in doubt.
|
|
145
|
-
- [
|
|
145
|
+
- [K] KNOWLEDGE — a durable truth that will hold across sessions: "the API is at
|
|
146
146
|
:8787", "uv is used for packages", "config lives in ~/.hicortex/". Not tied
|
|
147
147
|
to a single moment.
|
|
148
|
-
- [D]
|
|
148
|
+
- [D] DECISIONS — a choice made that future work builds on, and that a later
|
|
149
149
|
decision can SUPERSEDE: "switched from gemma4 to qwen3.5", "adopted the
|
|
150
|
-
graded-schema tag model". Not
|
|
150
|
+
graded-schema tag model". Not knowledge (it can change) and not experience
|
|
151
151
|
(it persists and constrains).
|
|
152
|
-
- NEVER use [L] (
|
|
153
|
-
not here. If the model emits [L], it is wrong — re-tag as
|
|
152
|
+
- NEVER use [L] (learnings). Learnings are extracted by a SEPARATE reflection stage,
|
|
153
|
+
not here. If the model emits [L], it is wrong — re-tag as experience/knowledge/decisions.
|
|
154
154
|
The type tag goes BEFORE the subject, never as a section/category bracket.
|
|
155
155
|
|
|
156
156
|
TOPIC-FIRST RULE (critical — read carefully):
|
|
@@ -159,8 +159,8 @@ concrete thing it is about — the system, file, component, decision area, or
|
|
|
159
159
|
entity. The subject is what a future reader would search for.
|
|
160
160
|
- Write: "[E] Electrical load calculation: don't bundle unknown loads into one figure — user rejected the estimate"
|
|
161
161
|
- NOT: "[E] User rejected AI's bundling of unknown loads"
|
|
162
|
-
- Write: "[
|
|
163
|
-
- NOT: "[
|
|
162
|
+
- Write: "[K] Nightly capture (Hermes): cron sessions are excluded — source='cron' is skipped before distillation"
|
|
163
|
+
- NOT: "[K] Discovered that cron sessions are filtered out"
|
|
164
164
|
Reason: each item's first words (after the type tag) become the memory's one-line
|
|
165
165
|
index entry AND dominate its search embedding. An item that opens with a category
|
|
166
166
|
label, a sentiment ("Strong Negative"), or "User rejected…" is unfindable — it
|