@gamaze/hicortex 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,8 +66,11 @@ The plugin connects to `http://127.0.0.1:8787` by default. For a remote server,
66
66
  | When | What | How |
67
67
  |------|------|-----|
68
68
  | Agent start | Standing context (`## Context`) + recent lessons fetched fresh and injected | CC SessionStart hook (calls `hicortex lessons-context`) / Hermes plugin `system_prompt_block` (+ `prefetch` recall) / OC `before_agent_start` hook |
69
+ | Every prompt (0.14) | A compact **recall index** of relevant memories is injected — one line per memory; the agent lazy-loads full content with `hicortex_get` only when needed | CC UserPromptSubmit hook (calls `hicortex recall-hook` → server `POST /recall-index`). Turn-based dedup per session; resets on new session/compaction. Fail-soft, 1 s timeout |
69
70
  | Nightly | Denoise sessions → POST /distill → server distills + embeds + stores → consolidate (score, reflect, link, decay) | Automatic pipeline — no manual steps |
70
71
 
72
+ **Exposure vs use (0.14):** appearing in the recall index only marks a memory as *shown* (it stops decaying while topically active); fetching it with `hicortex_get` marks it as *used* (durable strengthening). Memory importance is driven by what agents actually use, not by what was pushed at them.
73
+
71
74
  ## Memory Domains & Tags
72
75
 
73
76
  Domains are your top-level memory spheres — the handful of areas your life or work actually splits into. Every memory gets **multiple weighted tags** from your domain list plus one **primary** domain, so a memory that spans areas (a work project that touches your finances) lives in both instead of being forced into one bin. Domains drive the knowledge index, graph coloring, and lesson selection.
@@ -107,9 +110,10 @@ The run is resumable — interrupt it any time and it continues where it stopped
107
110
 
108
111
  ## Agent Tools (MCP)
109
112
 
110
- 8 tools available via MCP:
113
+ 9 tools available via MCP:
111
114
 
112
115
  - **hicortex_search** — Semantic search across all stored memories
116
+ - **hicortex_get** — Fetch one memory's full content by id (0.14) — the lazy-load counterpart of the recall index; fetching marks the memory as used
113
117
  - **hicortex_recent** — Get recent decisions and project state (queryless recall; renamed in 0.12)
114
118
  - **hicortex_ingest** — Store a memory directly
115
119
  - **hicortex_lessons** — Get actionable lessons from reflection
@@ -203,6 +207,14 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
203
207
  | `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) |
204
208
  | `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 |
205
209
  | `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 |
210
+ | `decayHalfLifeDays` | Memory decay half-life in days at reference importance (default: 365). Larger = slower forgetting; importance, access, and links slow it further |
211
+ | `searchLimit` / `recentLimit` | Default result counts for search (8) and recent (12) |
212
+ | `recentWindowDays` | Candidate window for recent recall (default: 180) |
213
+ | `coldExposureSlots` | Top-k slots reservable for never-accessed memories so the long tail gets exposure (default: 2) |
214
+ | `recallMaxItems` | Max lines in the pushed recall index (default: 6) |
215
+ | `recallMinSimilarity` | Relevance floor for index entries (default: 0.55; text-search matches always pass) |
216
+ | `recallReshowTurns` | Turns before an already-shown memory may reappear in the same session (default: 30) |
217
+ | `recallMinPromptChars` | Prompts shorter than this skip the recall index (default: 20) |
206
218
  | `telemetry` | Anonymous usage telemetry, `false` to opt out |
207
219
 
208
220
  Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze.com/docs/configuration.html)
@@ -214,6 +226,8 @@ Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze
214
226
  | `/health` | GET | No | Server status, memory count, version |
215
227
  | `/distill` | POST | Yes | Canonical capture endpoint (0.9.0+). Accepts denoised session text (`text` string or `messages` array), distills server-side, stores. Used by both server-mode and client-mode nightly jobs. |
216
228
  | `/search` | GET | Yes | Semantic memory search |
229
+ | `/recall-index` | POST | Yes | Pushed recall index (0.14): `{session_id, prompt}` → compact one-line-per-memory block (or `null`); `{session_id, reset: true}` clears the session's dedup state. Appearing in the index marks memories *shown*, never *used* |
230
+ | `/memory` | GET | Yes | Fetch one memory by `?id=` (0.14). Marks it as used (strengthens) — the lazy-load counterpart of `/recall-index` |
217
231
  | `/recent` | GET | Yes | Recent memories, queryless recall (renamed from `/context` in 0.12) |
218
232
  | `/context` | GET / PUT | Yes | Standing [context layer](#context-layer): read all sections / partial-upsert named sections. `?agent=<id>` selects a [per-agent scope](#per-agent-context-013) (server resolves override/global/off + merge); invalid id → 400. Recall-style query params on GET → 400 (use `/recent`) |
219
233
  | `/context/ui` | GET | No* | Web editor for the context layer (shell served without auth, like `/viz`; data via `/context`) |
package/dist/cli.js CHANGED
@@ -165,6 +165,17 @@ switch (command) {
165
165
  });
166
166
  });
167
167
  break;
168
+ case "recall-hook":
169
+ // CC UserPromptSubmit + SessionStart hook: pushed recall index (#192).
170
+ // Reads the CC hook payload from stdin, POSTs to the server, prints the
171
+ // index block (UserPromptSubmit) or resets session dedup (SessionStart).
172
+ // Fail-soft — any error = silent exit 0, never blocks a CC session.
173
+ import("./recall-hook-cli.js").then(({ runRecallHook }) => {
174
+ runRecallHook()
175
+ .then(() => process.exit(0))
176
+ .catch(() => process.exit(0));
177
+ }).catch(() => process.exit(0));
178
+ break;
168
179
  case "lessons-context":
169
180
  // CC SessionStart hook: fetch lessons from the configured server and print
170
181
  // a Markdown block to stdout. Fail-soft — any error = silent exit 0 so a
@@ -196,6 +207,7 @@ Commands:
196
207
  relink Resumable link-discovery pass over the ENTIRE corpus (server mode)
197
208
  classify-domains Backfill content-based domain tags over the corpus (server mode, needs config.domains)
198
209
  lessons-context Fetch lessons and print Markdown to stdout (CC SessionStart hook)
210
+ recall-hook Pushed recall index for the current prompt (CC UserPromptSubmit/SessionStart hook)
199
211
  context Standing context layer (show|edit) against the configured server
200
212
  status Show current configuration and stats
201
213
  uninstall Remove CC integration (preserves DB)
@@ -12,7 +12,7 @@ import { type DomainDef } from "./domain-classify.js";
12
12
  * Minimum COSINE similarity for a link candidate.
13
13
  *
14
14
  * Calibration (2026-07): measured top-10 neighbor cosine histogram on the
15
- * 2945-memory production corpus (bedrock). Typical top-1 neighbor cosine:
15
+ * ~3000-memory production corpus. Typical top-1 neighbor cosine:
16
16
  * median 0.823, p10 0.743, p90 0.902. Threshold 0.75 combined with the
17
17
  * top-3 cap yields ≈ 2.2 candidate links/memory. The previous value (0.55)
18
18
  * lived on an accidental 1−L2 scale where it required cosine > 0.90 — a
@@ -65,7 +65,7 @@ const CONSOLIDATE_PRUNE_MIN_AGE_DAYS = 90;
65
65
  * Minimum COSINE similarity for a link candidate.
66
66
  *
67
67
  * Calibration (2026-07): measured top-10 neighbor cosine histogram on the
68
- * 2945-memory production corpus (bedrock). Typical top-1 neighbor cosine:
68
+ * ~3000-memory production corpus. Typical top-1 neighbor cosine:
69
69
  * median 0.823, p10 0.743, p90 0.902. Threshold 0.75 combined with the
70
70
  * top-3 cap yields ≈ 2.2 candidate links/memory. The previous value (0.55)
71
71
  * lived on an accidental 1−L2 scale where it required cosine > 0.90 — a
@@ -261,7 +261,7 @@ function extractAgentFlag(args) {
261
261
  // A missing value (end of args) or the next token being another flag is a
262
262
  // typo — never let it silently fall through to the global scope.
263
263
  if (val === undefined || val.startsWith("-")) {
264
- throw new ContextCliError("--agent requires a value, e.g. --agent lenny");
264
+ throw new ContextCliError("--agent requires a value, e.g. --agent alice");
265
265
  }
266
266
  agent = val;
267
267
  i++;
package/dist/db.js CHANGED
@@ -321,6 +321,20 @@ const MIGRATIONS = [
321
321
  `);
322
322
  },
323
323
  },
324
+ {
325
+ version: 8,
326
+ name: "add_shown_count",
327
+ up: (db) => {
328
+ // #192 recall alignment: exposure tracking separate from use. shown_count
329
+ // counts appearances in the pushed recall index (/recall-index), which
330
+ // refreshes last_accessed (mild strengthen: decay clock resets) but does
331
+ // NOT touch access_count — hardening, the prune shield, and the adoption
332
+ // metric (uses per showing) stay driven by real use only.
333
+ if (!hasColumn(db, "memories", "shown_count")) {
334
+ db.exec("ALTER TABLE memories ADD COLUMN shown_count INTEGER DEFAULT 0");
335
+ }
336
+ },
337
+ },
324
338
  ];
325
339
  /**
326
340
  * Run all pending migrations against the database.
@@ -5,13 +5,13 @@
5
5
  * ---------------
6
6
  * The nightly's legacy `stageDomainCuration` groups PROJECTS into domains and
7
7
  * assigns every memory its project's domain. For an owner whose "projects" are
8
- * often AGENT names (lenny, nano, ...), one agent produces memories spanning
8
+ * often AGENT names (alice, bob, ...), one agent produces memories spanning
9
9
  * many life areas, so life-memories get smeared under the agent. This module
10
10
  * classifies a single memory into life-spheres by its CONTENT, drawn from a
11
11
  * user-curated vocabulary in ~/.hicortex/config.json (`domains`).
12
12
  *
13
13
  * GRADED SCHEMA TAGS (spec 2026-07-07, supersedes the LLM-picked primary from
14
- * PR #152/#153): a memory genuinely spans spheres — "set up bedrock for the
14
+ * PR #152/#153): a memory genuinely spans spheres — "set up the server for the
15
15
  * agent fleet" is both Hardware AND Ventures. The classifier now returns ONLY
16
16
  * the discrete part:
17
17
  * - `tags`: 0..N vocabulary names that genuinely apply, MOST-RELEVANT FIRST
@@ -31,7 +31,7 @@
31
31
  *
32
32
  * The `project` name is passed to the classifier as a HINT (content wins;
33
33
  * project only breaks ties). This rescues terse technical memories from
34
- * projects like raider/hiops/catalyst whose content alone reads as ambiguous.
34
+ * projects whose content alone reads as ambiguous.
35
35
  *
36
36
  * The classifier makes ONE constrained LLM call per memory (via the classify
37
37
  * tier — classifyModel/classifyBaseUrl when configured, else the reflect
@@ -6,13 +6,13 @@
6
6
  * ---------------
7
7
  * The nightly's legacy `stageDomainCuration` groups PROJECTS into domains and
8
8
  * assigns every memory its project's domain. For an owner whose "projects" are
9
- * often AGENT names (lenny, nano, ...), one agent produces memories spanning
9
+ * often AGENT names (alice, bob, ...), one agent produces memories spanning
10
10
  * many life areas, so life-memories get smeared under the agent. This module
11
11
  * classifies a single memory into life-spheres by its CONTENT, drawn from a
12
12
  * user-curated vocabulary in ~/.hicortex/config.json (`domains`).
13
13
  *
14
14
  * GRADED SCHEMA TAGS (spec 2026-07-07, supersedes the LLM-picked primary from
15
- * PR #152/#153): a memory genuinely spans spheres — "set up bedrock for the
15
+ * PR #152/#153): a memory genuinely spans spheres — "set up the server for the
16
16
  * agent fleet" is both Hardware AND Ventures. The classifier now returns ONLY
17
17
  * the discrete part:
18
18
  * - `tags`: 0..N vocabulary names that genuinely apply, MOST-RELEVANT FIRST
@@ -32,7 +32,7 @@
32
32
  *
33
33
  * The `project` name is passed to the classifier as a HINT (content wins;
34
34
  * project only breaks ties). This rescues terse technical memories from
35
- * projects like raider/hiops/catalyst whose content alone reads as ambiguous.
35
+ * projects whose content alone reads as ambiguous.
36
36
  *
37
37
  * The classifier makes ONE constrained LLM call per memory (via the classify
38
38
  * tier — classifyModel/classifyBaseUrl when configured, else the reflect
@@ -2,7 +2,7 @@
2
2
  * Hermes transcript reader — the nightly capture path for Nous Research Hermes.
3
3
  *
4
4
  * Hermes stores conversation in a SQLite state DB, one per profile:
5
- * ~/.hermes/profiles/<profile>/state.db (per-profile agents: lenny, raider, nano)
5
+ * ~/.hermes/profiles/<profile>/state.db (per-profile agents: alice, bob, carol)
6
6
  * ~/.hermes/state.db (global, non-profile setups)
7
7
  *
8
8
  * Schema (relevant columns):
@@ -3,7 +3,7 @@
3
3
  * Hermes transcript reader — the nightly capture path for Nous Research Hermes.
4
4
  *
5
5
  * Hermes stores conversation in a SQLite state DB, one per profile:
6
- * ~/.hermes/profiles/<profile>/state.db (per-profile agents: lenny, raider, nano)
6
+ * ~/.hermes/profiles/<profile>/state.db (per-profile agents: alice, bob, carol)
7
7
  * ~/.hermes/state.db (global, non-profile setups)
8
8
  *
9
9
  * Schema (relevant columns):
package/dist/init.d.ts CHANGED
@@ -119,6 +119,13 @@ export declare function isEphemeralNpxPath(binPath: string): boolean;
119
119
  * @param settingsPath Override for the settings.json path (used in tests; defaults to CC_SETTINGS).
120
120
  */
121
121
  export declare function installSessionStartHook(settingsPath?: string): void;
122
+ /**
123
+ * Install (or verify) the #192 pushed-recall hooks: `hicortex recall-hook`
124
+ * under UserPromptSubmit (per-prompt recall index) AND under SessionStart
125
+ * (per-session dedup reset — the CLI dispatches on the payload's
126
+ * hook_event_name, so one command serves both events).
127
+ */
128
+ export declare function installRecallHooks(settingsPath?: string): void;
122
129
  export declare function runInit(options?: {
123
130
  serverUrl?: string;
124
131
  agentName?: string;
package/dist/init.js CHANGED
@@ -27,6 +27,7 @@ exports.decideAgentName = decideAgentName;
27
27
  exports.scaffoldDefaultDomains = scaffoldDefaultDomains;
28
28
  exports.isEphemeralNpxPath = isEphemeralNpxPath;
29
29
  exports.installSessionStartHook = installSessionStartHook;
30
+ exports.installRecallHooks = installRecallHooks;
30
31
  exports.runInit = runInit;
31
32
  exports.resolveNightlyHour = resolveNightlyHour;
32
33
  const paths_js_1 = require("./paths.js");
@@ -960,10 +961,30 @@ function resolveBinaryArgs() {
960
961
  * @param settingsPath Override for the settings.json path (used in tests; defaults to CC_SETTINGS).
961
962
  */
962
963
  function installSessionStartHook(settingsPath) {
964
+ installCcHook("SessionStart", "lessons-context", 10, settingsPath);
965
+ }
966
+ /**
967
+ * Install (or verify) the #192 pushed-recall hooks: `hicortex recall-hook`
968
+ * under UserPromptSubmit (per-prompt recall index) AND under SessionStart
969
+ * (per-session dedup reset — the CLI dispatches on the payload's
970
+ * hook_event_name, so one command serves both events).
971
+ */
972
+ function installRecallHooks(settingsPath) {
973
+ installCcHook("UserPromptSubmit", "recall-hook", 3, settingsPath);
974
+ installCcHook("SessionStart", "recall-hook", 3, settingsPath);
975
+ }
976
+ /**
977
+ * Shared CC-hook installer: add `hicortex <subcommand>` under the given hook
978
+ * event in ~/.claude/settings.json. Idempotent per (event, subcommand): skips
979
+ * if any existing entry for that event already runs the subcommand. `timeout`
980
+ * is CC's hook-process kill timeout in SECONDS (the network timeout inside the
981
+ * command is separate and shorter).
982
+ */
983
+ function installCcHook(eventName, subcommand, timeout, settingsPath) {
963
984
  const targetPath = settingsPath ?? CC_SETTINGS;
964
985
  const binaryArgs = resolveBinaryArgs();
965
- // Build the command string: "/path/to/hicortex lessons-context" or "npx -y @gamaze/hicortex lessons-context"
966
- const command = [...binaryArgs, "lessons-context"].join(" ");
986
+ // e.g. "/path/to/hicortex lessons-context" or "npx -y @gamaze/hicortex recall-hook"
987
+ const command = [...binaryArgs, subcommand].join(" ");
967
988
  let settings = {};
968
989
  if ((0, node_fs_1.existsSync)(targetPath)) {
969
990
  try {
@@ -971,23 +992,26 @@ function installSessionStartHook(settingsPath) {
971
992
  }
972
993
  catch {
973
994
  // File exists but is malformed — do NOT overwrite (would destroy the user's entire CC config).
974
- console.log(` ⚠ ${targetPath} exists but is not valid JSON — skipping SessionStart hook.`);
995
+ console.log(` ⚠ ${targetPath} exists but is not valid JSON — skipping ${eventName} hook.`);
975
996
  console.log(` Fix the file, then re-run init, or add the hook manually:`);
976
997
  console.log(` command: "${command}"`);
977
998
  return;
978
999
  }
979
1000
  }
980
- // Ensure hooks object and SessionStart array exist
1001
+ // Ensure hooks object and the event array exist
981
1002
  if (!settings.hooks || typeof settings.hooks !== "object") {
982
1003
  settings.hooks = {};
983
1004
  }
984
1005
  const hooks = settings.hooks;
985
- if (!Array.isArray(hooks.SessionStart)) {
986
- hooks.SessionStart = [];
987
- }
988
- const sessionStart = hooks.SessionStart;
989
- // Idempotent: skip if any existing entry's command contains "lessons-context"
990
- const alreadyInstalled = sessionStart.some((entry) => {
1006
+ if (!Array.isArray(hooks[eventName])) {
1007
+ hooks[eventName] = [];
1008
+ }
1009
+ const entries = hooks[eventName];
1010
+ // Idempotent: skip if any existing entry's command runs this subcommand.
1011
+ // Word-boundary guard so "recall-hook" never matches a hypothetical
1012
+ // "recall-hook-foo" command.
1013
+ const subcommandRe = new RegExp(`(^|\\s)${subcommand}(\\s|$)`);
1014
+ const alreadyInstalled = entries.some((entry) => {
991
1015
  if (typeof entry !== "object" || entry === null)
992
1016
  return false;
993
1017
  const e = entry;
@@ -997,21 +1021,21 @@ function installSessionStartHook(settingsPath) {
997
1021
  if (typeof h !== "object" || h === null)
998
1022
  return false;
999
1023
  const hook = h;
1000
- return typeof hook.command === "string" && hook.command.includes("lessons-context");
1024
+ return typeof hook.command === "string" && subcommandRe.test(hook.command);
1001
1025
  });
1002
1026
  }
1003
1027
  return false;
1004
1028
  });
1005
1029
  if (alreadyInstalled) {
1006
- console.log(` ✓ SessionStart hook already installed`);
1030
+ console.log(` ✓ ${eventName} hook (${subcommand}) already installed`);
1007
1031
  return;
1008
1032
  }
1009
- sessionStart.push({
1010
- hooks: [{ type: "command", command, timeout: 10 }],
1033
+ entries.push({
1034
+ hooks: [{ type: "command", command, timeout }],
1011
1035
  });
1012
1036
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(targetPath), { recursive: true });
1013
1037
  (0, node_fs_1.writeFileSync)(targetPath, JSON.stringify(settings, null, 2));
1014
- console.log(` ✓ Installed SessionStart hook: ${command}`);
1038
+ console.log(` ✓ Installed ${eventName} hook: ${command}`);
1015
1039
  }
1016
1040
  function installLaunchd(binaryArgs) {
1017
1041
  const plistDir = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents");
@@ -1263,6 +1287,8 @@ async function runInit(options = {}) {
1263
1287
  // Install CC SessionStart hook for query-time lesson injection.
1264
1288
  // Lessons are now fetched live at session start — no static CLAUDE.md block needed.
1265
1289
  installSessionStartHook();
1290
+ // #192: per-prompt pushed recall index + session dedup reset.
1291
+ installRecallHooks();
1266
1292
  // Strip the old static lessons block from CLAUDE.md (0.9.0 migration).
1267
1293
  // Lessons are now delivered via the SessionStart hook instead.
1268
1294
  const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
@@ -1418,8 +1444,10 @@ async function runClientInit(serverUrl, agentName) {
1418
1444
  allowHicortexTools();
1419
1445
  // Step 5: Install CC commands
1420
1446
  installCcCommands();
1421
- // Step 6: Install SessionStart hook for query-time lessons.
1447
+ // Step 6: Install SessionStart hook for query-time lessons + the #192
1448
+ // per-prompt pushed-recall hooks.
1422
1449
  installSessionStartHook();
1450
+ installRecallHooks();
1423
1451
  // Strip the old static CLAUDE.md lessons block if present (0.9.0 migration).
1424
1452
  const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
1425
1453
  if ((0, claude_md_js_1.removeLessonsBlock)(claudeMdPath)) {
@@ -33,6 +33,20 @@ export interface ContextResponse {
33
33
  agent?: string;
34
34
  mode?: string;
35
35
  }
36
+ export interface ResolvedConfig {
37
+ serverUrl: string;
38
+ authToken: string | undefined;
39
+ home: string;
40
+ /** Per-agent context id sent as ?agent= (0.13); null → global (no param). */
41
+ agentName: string | null;
42
+ }
43
+ /**
44
+ * Read ~/.hicortex/config.json and resolve the server URL + auth token, or
45
+ * null when there is no usable config (server not set up yet — fail soft).
46
+ * Exported for reuse by the recall-hook CLI (#192) so the two CC hooks can
47
+ * never resolve the server differently.
48
+ */
49
+ export declare function resolveConfig(): ResolvedConfig | null;
36
50
  /**
37
51
  * Title-case a section name for its heading: split on `-`/`_`, capitalize each
38
52
  * word ("user" → "User", "my_notes" → "My Notes").
@@ -69,7 +83,7 @@ export declare function renderContextBlock(sections: Record<string, string>): st
69
83
  * into every persona; on a bare fetch (no id) the guard is off (amendment
70
84
  * A2).
71
85
  * - CC passes `false` ALWAYS and deliberately (see the call site): a thin CC
72
- * client auto-upgrades via npx BEFORE bedrock does, so during the upgrade
86
+ * client auto-upgrades via npx BEFORE the server does, so during the upgrade
73
87
  * window it talks to a 0.12 server that cannot hold ANY per-agent config —
74
88
  * global IS the operator's intended state there, and a guard would instead
75
89
  * blank ALL context for every CC session in that window.
@@ -23,6 +23,7 @@
23
23
  * CC session, and a broken /context fetch must never blank the whole output.
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.resolveConfig = resolveConfig;
26
27
  exports.titleCaseSection = titleCaseSection;
27
28
  exports.orderSectionNames = orderSectionNames;
28
29
  exports.renderContextBlock = renderContextBlock;
@@ -41,6 +42,8 @@ const THIS_HARNESS = "cc";
41
42
  /**
42
43
  * Read ~/.hicortex/config.json and resolve the server URL + auth token, or
43
44
  * null when there is no usable config (server not set up yet — fail soft).
45
+ * Exported for reuse by the recall-hook CLI (#192) so the two CC hooks can
46
+ * never resolve the server differently.
44
47
  */
45
48
  function resolveConfig() {
46
49
  const home = (0, paths_js_1.hicortexHome)();
@@ -180,7 +183,7 @@ function renderContextBlock(sections) {
180
183
  * into every persona; on a bare fetch (no id) the guard is off (amendment
181
184
  * A2).
182
185
  * - CC passes `false` ALWAYS and deliberately (see the call site): a thin CC
183
- * client auto-upgrades via npx BEFORE bedrock does, so during the upgrade
186
+ * client auto-upgrades via npx BEFORE the server does, so during the upgrade
184
187
  * window it talks to a 0.12 server that cannot hold ANY per-agent config —
185
188
  * global IS the operator's intended state there, and a guard would instead
186
189
  * blank ALL context for every CC session in that window.
@@ -216,7 +219,7 @@ async function fetchContextBlock(cfg) {
216
219
  return null;
217
220
  const data = await resp.json();
218
221
  // CC deliberately passes requireAgentEcho: false (NOT the OC/Hermes old-server
219
- // guard). A thin CC client auto-upgrades via npx BEFORE bedrock does, so
222
+ // guard). A thin CC client auto-upgrades via npx BEFORE the server does, so
220
223
  // mid-upgrade it may hit a 0.12 server that returns global context with no
221
224
  // `agent` echo — and a 0.12 server cannot hold per-agent config, so global is
222
225
  // the intended state. Guarding here would blank ALL CC context in that window.
@@ -64,6 +64,8 @@ const graph_js_1 = require("./graph.js");
64
64
  const viz_js_1 = require("./viz.js");
65
65
  const context_store_js_1 = require("./context-store.js");
66
66
  const retrieval = __importStar(require("./retrieval.js"));
67
+ const recall_registry_js_1 = require("./recall-registry.js");
68
+ const recall_index_js_1 = require("./recall-index.js");
67
69
  const seed_lesson_js_1 = require("./seed-lesson.js");
68
70
  const distiller_js_1 = require("./distiller.js");
69
71
  // ---------------------------------------------------------------------------
@@ -85,6 +87,9 @@ let contextClients = ["cc"];
85
87
  // Read once at boot (like contextClients); the drop-in-a-dir presence path is
86
88
  // per-request, so only explicit config entries need a daemon restart to apply.
87
89
  let contextAgents = {};
90
+ // Pushed-recall dedup registry (#192) + options; configured at boot.
91
+ let recallRegistry = new recall_registry_js_1.SessionRecallRegistry();
92
+ let recallIndexOptions = {};
88
93
  // Cache detectChunkSize results keyed by "<provider>/<model>@<baseUrl>" so we
89
94
  // probe each endpoint once per server boot rather than once per /distill request.
90
95
  const chunkSizeCache = new Map();
@@ -103,9 +108,9 @@ function createMcpServer() {
103
108
  version: VERSION,
104
109
  });
105
110
  // -- hicortex_search --
106
- server.tool("hicortex_search", "Search long-term memory using semantic similarity. Returns the most relevant memories from past sessions.", {
111
+ server.tool("hicortex_search", "Search shared long-term memory (all agents, all sessions). CALL THIS BEFORE assuming, guessing, or asking the user about anything that may have come up before: prior decisions, preferences, project facts, people, hardware, past incidents. If you are about to write 'I don't have information about…', search first.", {
107
112
  query: zod_1.z.string().describe("Search query text"),
108
- limit: zod_1.z.coerce.number().optional().describe("Max results (default 5)"),
113
+ limit: zod_1.z.coerce.number().optional().describe("Max results (default: server config searchLimit)"),
109
114
  project: zod_1.z.string().optional().describe("Filter by project name"),
110
115
  }, async ({ query, limit, project }) => {
111
116
  if (!db)
@@ -118,10 +123,29 @@ function createMcpServer() {
118
123
  return { content: [{ type: "text", text: `Search failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
119
124
  }
120
125
  });
126
+ // -- hicortex_get --
127
+ server.tool("hicortex_get", "Fetch ONE memory's full content by id — use this to lazy-load entries from the '## Memory recall (auto)' index or from search results whose snippet was not enough. Fetching a memory marks it as used (strengthens it), so only fetch what you actually need.", {
128
+ id: zod_1.z.string().describe("Memory id (as shown in recall index/search results)"),
129
+ }, async ({ id }) => {
130
+ if (!db)
131
+ return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
132
+ try {
133
+ const mem = storage.getMemory(db, id);
134
+ if (!mem)
135
+ return { content: [{ type: "text", text: `No memory with id ${id}` }], isError: true };
136
+ // Real use → full strengthen (access_count + hardening + prune shield).
137
+ storage.strengthenMemory(db, id, new Date().toISOString());
138
+ const header = `[${mem.memory_type ?? "episode"}] ${mem.project ?? ""} ${mem.created_at ?? ""}`.trim();
139
+ return { content: [{ type: "text", text: `${header}\n\n${mem.content ?? ""}` }] };
140
+ }
141
+ catch (err) {
142
+ return { content: [{ type: "text", text: `Get failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
143
+ }
144
+ });
121
145
  // -- hicortex_recent --
122
- server.tool("hicortex_recent", "Get recent memories, optionally filtered by project. Queryless recall of the latest memories by project, ranked by importance. Useful to catch up on what happened recently.", {
146
+ server.tool("hicortex_recent", "Get recent memories, optionally filtered by project. CALL THIS AT THE START of substantive work on a project to catch up on its latest state — cheaper than asking the user what happened.", {
123
147
  project: zod_1.z.string().optional().describe("Filter by project name"),
124
- limit: zod_1.z.coerce.number().optional().describe("Max results (default 10)"),
148
+ limit: zod_1.z.coerce.number().optional().describe("Max results (default: server config recentLimit)"),
125
149
  }, async ({ project, limit }) => {
126
150
  if (!db)
127
151
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
@@ -446,6 +470,21 @@ async function startServer(options = {}) {
446
470
  // agents/<id> dir onto disk takes effect immediately (per-request presence).
447
471
  const resolvedAgents = (0, context_store_js_1.resolveContextAgents)(savedConfig?.contextAgents);
448
472
  contextAgents = resolvedAgents.agents;
473
+ // #192 recall/decay alignment: decay speed + recall breadth + pushed-recall
474
+ // knobs, ALL from config (see retrieval.ts configureRecall for the key list)
475
+ // so calibration is a config edit + restart, never a release.
476
+ retrieval.configureDecay({ halfLifeDays: savedConfig?.decayHalfLifeDays });
477
+ const recallCfg = retrieval.configureRecall(savedConfig);
478
+ console.log(`[hicortex] Recall: k=${recallCfg.searchLimit}/recent=${recallCfg.recentLimit}` +
479
+ `/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots}`);
480
+ recallRegistry = new recall_registry_js_1.SessionRecallRegistry({
481
+ reshowTurns: savedConfig?.recallReshowTurns,
482
+ });
483
+ recallIndexOptions = {
484
+ minSimilarity: savedConfig?.recallMinSimilarity,
485
+ maxItems: savedConfig?.recallMaxItems,
486
+ minPromptLength: savedConfig?.recallMinPromptChars,
487
+ };
449
488
  if (resolvedAgents.dropped.length > 0) {
450
489
  console.warn(`[hicortex] Ignoring invalid contextAgents entries: ${resolvedAgents.dropped.join(", ")} ` +
451
490
  `(keys must match ^[a-z0-9][a-z0-9_-]*$; modes must be override|global|off)`);
@@ -582,7 +621,8 @@ async function startServer(options = {}) {
582
621
  res.status(400).json({ error: "Missing 'query'" });
583
622
  return;
584
623
  }
585
- const limit = req.query.limit ? Number(req.query.limit) : 5;
624
+ // No hardcoded default: absent limit config-driven (searchLimit).
625
+ const limit = req.query.limit ? Number(req.query.limit) : undefined;
586
626
  const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
587
627
  const privacy = typeof req.query.privacy === "string" && req.query.privacy
588
628
  ? req.query.privacy.split(",").map((s) => s.trim()).filter(Boolean)
@@ -595,6 +635,50 @@ async function startServer(options = {}) {
595
635
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
596
636
  }
597
637
  });
638
+ // REST /recall-index — pushed recall index (#192). One recall logic for all
639
+ // harnesses: CC UserPromptSubmit hook, Hermes/OC per-turn plugins. Returns a
640
+ // compact index block (or null); exposure is recorded as shown_count +
641
+ // last_accessed, NOT access_count (that stays reserved for hicortex_get /
642
+ // GET /memory — real use). {reset: true} clears the session's dedup state
643
+ // (SessionStart/compaction).
644
+ app.post("/recall-index", async (req, res) => {
645
+ if (!db) {
646
+ res.status(503).json({ error: "Server not initialized" });
647
+ return;
648
+ }
649
+ const r = await (0, recall_index_js_1.handleRecallIndex)({
650
+ db,
651
+ registry: recallRegistry,
652
+ retrieveFn: (query, limit) => retrieval.retrieve(db, embedder_js_1.embed, query, { limit, noStrengthen: true }),
653
+ options: recallIndexOptions,
654
+ }, req.body);
655
+ res.status(r.status).json(r.body);
656
+ });
657
+ // REST /memory?id= — fetch one memory's full content (lazy-load counterpart
658
+ // of /recall-index for REST clients: Hermes/OC plugins). Marks it as used.
659
+ app.get("/memory", (req, res) => {
660
+ if (!db) {
661
+ res.status(503).json({ error: "Server not initialized" });
662
+ return;
663
+ }
664
+ const id = typeof req.query.id === "string" ? req.query.id : "";
665
+ if (!id) {
666
+ res.status(400).json({ error: "Missing 'id'" });
667
+ return;
668
+ }
669
+ try {
670
+ const mem = storage.getMemory(db, id);
671
+ if (!mem) {
672
+ res.status(404).json({ error: `No memory with id ${id}` });
673
+ return;
674
+ }
675
+ storage.strengthenMemory(db, id, new Date().toISOString());
676
+ res.json({ memory: mem });
677
+ }
678
+ catch (err) {
679
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
680
+ }
681
+ });
598
682
  // REST /recent — recent memories, optionally filtered by project.
599
683
  app.get("/recent", (req, res) => {
600
684
  if (!db) {
@@ -602,7 +686,8 @@ async function startServer(options = {}) {
602
686
  return;
603
687
  }
604
688
  const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
605
- const limit = req.query.limit ? Number(req.query.limit) : 10;
689
+ // No hardcoded default: absent limit config-driven (recentLimit).
690
+ const limit = req.query.limit ? Number(req.query.limit) : undefined;
606
691
  const privacy = typeof req.query.privacy === "string" && req.query.privacy
607
692
  ? req.query.privacy.split(",").map((s) => s.trim()).filter(Boolean)
608
693
  : undefined;
package/dist/nightly.js CHANGED
@@ -66,6 +66,7 @@ const hermes_transcript_reader_js_1 = require("./hermes-transcript-reader.js");
66
66
  const pi_transcript_reader_js_1 = require("./pi-transcript-reader.js");
67
67
  const oc_transcript_reader_js_1 = require("./oc-transcript-reader.js");
68
68
  const features_js_1 = require("./features.js");
69
+ const retrieval_js_1 = require("./retrieval.js");
69
70
  const state_js_1 = require("./state.js");
70
71
  const capture_cursors_js_1 = require("./capture-cursors.js");
71
72
  const capture_js_1 = require("./capture.js");
@@ -209,6 +210,10 @@ async function runNightly(options = {}) {
209
210
  }
210
211
  const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
211
212
  const port = savedConfig?.port ?? 8787;
213
+ // #192: consolidation's decay/prune stage must score with the same clock as
214
+ // the server's retrieval path (config decayHalfLifeDays, default 365).
215
+ (0, retrieval_js_1.configureDecay)({ halfLifeDays: savedConfig?.decayHalfLifeDays });
216
+ (0, retrieval_js_1.configureRecall)(savedConfig);
212
217
  const modeLabel = captureOnly ? " (capture-only)" : dryRun ? " (dry run)" : "";
213
218
  console.log(`[hicortex] Nightly pipeline starting${modeLabel}`);
214
219
  if (captureOnly) {
@@ -16,14 +16,14 @@
16
16
  *
17
17
  * Directory layout:
18
18
  * ~/.pi/agent/sessions/
19
- * --home-agents-Agents-raider--/
19
+ * --home-alice-projects-myagent--/
20
20
  * 2026-04-10T18-37-44-615Z_<uuid>.jsonl
21
21
  * 2026-04-11T07-51-28-282Z_<uuid>.jsonl
22
22
  * --home-agents-Development-MAIC--/
23
23
  * ...
24
24
  *
25
- * The encoded-cwd uses double-dash separators: /home/agents/Agents/raider
26
- * becomes --home-agents-Agents-raider--. The session header's `cwd` field
25
+ * The encoded-cwd uses double-dash separators: /home/alice/projects/myagent
26
+ * becomes --home-alice-projects-myagent--. The session header's `cwd` field
27
27
  * is the canonical path; the directory name is a filesystem-safe encoding.
28
28
  */
29
29
  import type { TranscriptBatch, CursorMap } from "./transcript-reader.js";
@@ -17,14 +17,14 @@
17
17
  *
18
18
  * Directory layout:
19
19
  * ~/.pi/agent/sessions/
20
- * --home-agents-Agents-raider--/
20
+ * --home-alice-projects-myagent--/
21
21
  * 2026-04-10T18-37-44-615Z_<uuid>.jsonl
22
22
  * 2026-04-11T07-51-28-282Z_<uuid>.jsonl
23
23
  * --home-agents-Development-MAIC--/
24
24
  * ...
25
25
  *
26
- * The encoded-cwd uses double-dash separators: /home/agents/Agents/raider
27
- * becomes --home-agents-Agents-raider--. The session header's `cwd` field
26
+ * The encoded-cwd uses double-dash separators: /home/alice/projects/myagent
27
+ * becomes --home-alice-projects-myagent--. The session header's `cwd` field
28
28
  * is the canonical path; the directory name is a filesystem-safe encoding.
29
29
  */
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -157,7 +157,7 @@ function readPiTranscripts(since, sessionsDir = DEFAULT_PI_SESSIONS_DIR, cursors
157
157
  }
158
158
  /**
159
159
  * Extract the last path segment from a cwd as the project name.
160
- * /home/agents/Agents/raider → "raider"
160
+ * /home/alice/projects/myagent → "myagent"
161
161
  * Falls back to decoding the directory name if cwd is empty.
162
162
  */
163
163
  function deriveProjectName(cwd, encodedDir) {
@@ -165,7 +165,7 @@ function deriveProjectName(cwd, encodedDir) {
165
165
  const segments = cwd.split("/").filter(Boolean);
166
166
  return segments[segments.length - 1] ?? "unknown";
167
167
  }
168
- // Decode the Pi directory encoding: --home-agents-Agents-raider-- → raider
168
+ // Decode the Pi directory encoding: --home-alice-projects-myagent-- → myagent
169
169
  const decoded = encodedDir.replace(/^--/, "").replace(/--$/, "").split("-");
170
170
  return decoded[decoded.length - 1] ?? "unknown";
171
171
  }