@gamaze/hicortex 0.15.2 → 0.16.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/dist/index.js CHANGED
@@ -184,7 +184,7 @@ async function buildLessonsBlock(project) {
184
184
  * plugin sends every turn and carries no tuning constants. A 404 flips the
185
185
  * module-level guard so an old server is probed once per gateway process.
186
186
  */
187
- async function fetchRecallIndexBlock(sessionId, prompt) {
187
+ async function fetchRecallIndexBlock(sessionId, prompt, project) {
188
188
  if (recallIndexLatched())
189
189
  return null;
190
190
  if (!sessionId || !prompt) {
@@ -201,7 +201,16 @@ async function fetchRecallIndexBlock(sessionId, prompt) {
201
201
  }
202
202
  return null;
203
203
  }
204
- const { ok, status, data } = await serverPost("/recall-index", { session_id: sessionId, prompt }, RECALL_TIMEOUT_MS);
204
+ // #203 scope: send the gateway-supplied project so retrieval can apply a soft
205
+ // project-affinity boost (no hard filter — "no hard filters in brains").
206
+ // Absent ⇒ no scope sent ⇒ no-op (preserves pre-#203 behavior).
207
+ const body = {
208
+ session_id: sessionId,
209
+ prompt,
210
+ };
211
+ if (project)
212
+ body.project = project;
213
+ const { ok, status, data } = await serverPost("/recall-index", body, RECALL_TIMEOUT_MS);
205
214
  if (status === 404) {
206
215
  recallIndexRetryAtMs = Date.now() + RECALL_REPROBE_INTERVAL_MS;
207
216
  return null;
@@ -317,7 +326,7 @@ exports.default = {
317
326
  const [contextBlock, lessonsBlock, recallBlock] = await Promise.all([
318
327
  fetchOcContextBlock(agentId).catch(() => null),
319
328
  buildLessonsBlock(ctx?.project).catch(() => null),
320
- fetchRecallIndexBlock(ctx?.sessionId, event?.prompt).catch(() => null),
329
+ fetchRecallIndexBlock(ctx?.sessionId, event?.prompt, ctx?.project).catch(() => null),
321
330
  ]);
322
331
  const blocks = [contextBlock, lessonsBlock, recallBlock].filter((b) => b !== null && b !== "");
323
332
  if (blocks.length === 0)
@@ -378,7 +387,7 @@ exports.default = {
378
387
  }), { name: "hicortex_search" });
379
388
  api.registerTool((_ctx) => ({
380
389
  name: "hicortex_get",
381
- description: "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. When the memory shapes your answer, cite it as given in the response.",
390
+ description: "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 fetch entries that could change your action — not every shown one. When the memory shapes your answer, cite it as given in the response — mark a fetched memory `FETCHED` and a one-line entry cited unread `SNIPPET`; don't pass SNIPPET off as established.",
382
391
  parameters: {
383
392
  type: "object",
384
393
  properties: {
package/dist/init.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * - Register MCP server in CC settings
14
14
  * - Install CC SessionStart hook for query-time lessons
15
15
  * - Strip old static CLAUDE.md learnings block if present
16
- * - Install CC custom commands (/learn, /hicortex-activate)
16
+ * - Remove legacy pre-0.10 CC commands (/learn, /hicortex-activate) if present
17
17
  */
18
18
  import type { DomainDef } from "./types.js";
19
19
  /**
package/dist/init.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * - Register MCP server in CC settings
15
15
  * - Install CC SessionStart hook for query-time lessons
16
16
  * - Strip old static CLAUDE.md learnings block if present
17
- * - Install CC custom commands (/learn, /hicortex-activate)
17
+ * - Remove legacy pre-0.10 CC commands (/learn, /hicortex-activate) if present
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.GENERIC_DEFAULT_DOMAINS = void 0;
@@ -255,98 +255,50 @@ function allowHicortexTools() {
255
255
  console.log(` ✓ Added Hicortex tool permissions to ${CC_SETTINGS}`);
256
256
  }
257
257
  }
258
- function installCcCommands() {
259
- (0, node_fs_1.mkdirSync)(CC_COMMANDS_DIR, { recursive: true });
260
- // /learn command
261
- const learnContent = `---
262
- name: learn
263
- description: Save an explicit learning/insight to Hicortex long-term memory. Immediate storage, no nightly wait. Use when you discover something worth remembering across sessions.
264
- argument-hint: <learning to save>
265
- allowed-tools: mcp__hicortex__hicortex_ingest, mcp__hicortex__hicortex_search, mcp__hicortex__hicortex_recent, mcp__hicortex__hicortex_lessons
266
- ---
267
-
268
- # Save Learning to Hicortex
269
-
270
- When invoked with \`/learn <text>\`, store the learning in long-term memory via the Hicortex MCP tool.
271
-
272
- ## Steps
273
-
274
- 1. Parse the text after \`/learn\`
275
- 2. Clean it up into a clear, self-contained statement that will make sense months from now
276
- 3. Include the "why" when relevant
277
- 4. Add today's date for temporal context
278
- 5. Call the \`hicortex_ingest\` tool with:
279
- - \`content\`: The learning text prefixed with "LEARNING: " and suffixed with the date
280
- - \`project\`: "global" (unless clearly project-specific)
281
- - \`memory_type\`: "lesson"
282
- 6. Confirm what was saved (brief, one line)
283
-
284
- ## Example
285
-
286
- \`/learn always check provider docs before assuming an API uses the same auth scheme as OpenAI\`
287
-
288
- Becomes a call to hicortex_ingest with:
289
- - content: "LEARNING: always check provider docs before assuming an API uses the same auth scheme as OpenAI — header names and token formats vary widely (Bearer vs x-api-key vs custom)."
290
- - memory_type: "lesson"
291
- `;
292
- const learnPath = (0, node_path_1.join)(CC_COMMANDS_DIR, "learn.md");
293
- if ((0, node_fs_1.existsSync)(learnPath)) {
294
- // Check if it's ours (contains hicortex_ingest)
295
- const existing = (0, node_fs_1.readFileSync)(learnPath, "utf-8");
296
- if (!existing.includes("hicortex_ingest") && !existing.includes("hicortex")) {
297
- console.log(` ⚠ Skipping /learn — existing command found (not Hicortex). Won't overwrite.`);
258
+ function cleanupLegacyCcCommands() {
259
+ // Pre-0.10 installs wrote two CC slash commands that are now RETIRED:
260
+ // - /learn : manual immediate ingest. Capture has been automatic
261
+ // (nightly-from-logs) since 0.9; hicortex_ingest
262
+ // remains for *explicitly requested* learnings, but
263
+ // no longer warrants a slash command.
264
+ // - /hicortex-activate : registered a commercial license key. licenseKey
265
+ // gates nothing now (the per-install auth TOKEN is
266
+ // the credential, auto-generated at init), so the
267
+ // command is dead.
268
+ // Remove stale copies so upgraders don't keep dead commands. Idempotent —
269
+ // a best-effort cleanup of our own files; never throws.
270
+ for (const name of ["learn.md", "hicortex-activate.md"]) {
271
+ const p = (0, node_path_1.join)(CC_COMMANDS_DIR, name);
272
+ if (!(0, node_fs_1.existsSync)(p))
273
+ continue;
274
+ // Ownership guard: only remove a file we actually wrote. `learn.md` is a
275
+ // generic command name a user may own independently deleting by filename
276
+ // alone would silently destroy their file. The retired writer always
277
+ // embedded "hicortex" (the ingest tool + prose); mirror the same marker the
278
+ // old installer used before overwriting. Skip + warn on anything else.
279
+ try {
280
+ if (!(0, node_fs_1.readFileSync)(p, "utf-8").toLowerCase().includes("hicortex")) {
281
+ console.log(` ⚠ Skipping ${name} in ${CC_COMMANDS_DIR} — not a Hicortex file, left untouched`);
282
+ continue;
283
+ }
298
284
  }
299
- else {
300
- (0, node_fs_1.writeFileSync)(learnPath, learnContent);
285
+ catch {
286
+ // Unreadable — do not delete blind; leave it and move on.
287
+ continue;
288
+ }
289
+ try {
290
+ (0, node_fs_1.rmSync)(p);
291
+ console.log(` ✓ Removed legacy command ${name} from ${CC_COMMANDS_DIR} (retired pre-0.10)`);
292
+ }
293
+ catch (err) {
294
+ // ENOENT = already gone (fine, idempotent). Anything else (EACCES, EBUSY)
295
+ // is worth a line so a stuck stale file is diagnosable — but never fatal
296
+ // to init (best-effort cleanup of our own file).
297
+ if (err?.code !== "ENOENT") {
298
+ console.log(` ⚠ Could not remove legacy command ${name}: ${err?.message ?? err}`);
299
+ }
301
300
  }
302
301
  }
303
- else {
304
- (0, node_fs_1.writeFileSync)(learnPath, learnContent);
305
- }
306
- // /hicortex-activate command — registers a commercial license key for display in status
307
- const activateContent = `---
308
- name: hicortex-activate
309
- description: Register a Hicortex commercial license key. Personal and noncommercial use is free; commercial use requires a per-seat license from hicortex.gamaze.com.
310
- argument-hint: <license-key>
311
- allowed-tools: Bash(mkdir:*), Bash(echo:*), Bash(launchctl:*), Bash(systemctl:*), Bash(curl:*), mcp__hicortex__hicortex_ingest, mcp__hicortex__hicortex_search, mcp__hicortex__hicortex_recent, mcp__hicortex__hicortex_lessons
312
- ---
313
-
314
- # Register Hicortex Commercial License
315
-
316
- ## If key provided (e.g. /hicortex-activate hctx-abc123)
317
-
318
- 1. Write the key to the config file:
319
-
320
- \`\`\`bash
321
- mkdir -p ~/.hicortex
322
- echo '{ "licenseKey": "THE_KEY_HERE" }' > ~/.hicortex/config.json
323
- \`\`\`
324
-
325
- 2. Restart the server to apply:
326
-
327
- On macOS:
328
- \`\`\`bash
329
- launchctl kickstart -k gui/$(id -u)/com.gamaze.hicortex
330
- \`\`\`
331
-
332
- On Linux:
333
- \`\`\`bash
334
- systemctl --user restart hicortex
335
- \`\`\`
336
-
337
- 3. Verify the key is recognised:
338
- \`\`\`bash
339
- hicortex status
340
- \`\`\`
341
-
342
- 4. Tell the user: "Commercial license registered. The license tier will appear in \`hicortex status\`."
343
-
344
- ## If no key provided
345
-
346
- Tell them: "Hicortex is free for personal and noncommercial use. Commercial use requires a per-seat license — see https://hicortex.gamaze.com/. After purchase you will receive a key; pass it here and I'll register it."
347
- `;
348
- (0, node_fs_1.writeFileSync)((0, node_path_1.join)(CC_COMMANDS_DIR, "hicortex-activate.md"), activateContent);
349
- console.log(` ✓ Installed /learn and /hicortex-activate commands in ${CC_COMMANDS_DIR}`);
350
302
  }
351
303
  // ---------------------------------------------------------------------------
352
304
  // Hermes setup
@@ -1211,7 +1163,6 @@ async function runInit(options = {}) {
1211
1163
  actions.push("Register MCP server in CC settings");
1212
1164
  if (d.hermesFound)
1213
1165
  actions.push("Install Hermes plugin + configure");
1214
- actions.push("Install /learn and /hicortex-activate commands");
1215
1166
  actions.push("Install SessionStart hook (query-time lessons)");
1216
1167
  if (actions.length === 0) {
1217
1168
  console.log("Everything is already configured. Nothing to do.");
@@ -1294,8 +1245,8 @@ async function runInit(options = {}) {
1294
1245
  }
1295
1246
  // Ensure tool permissions are set (also needed for users upgrading from older versions)
1296
1247
  allowHicortexTools();
1297
- // Install CC commands
1298
- installCcCommands();
1248
+ // Remove legacy pre-0.10 CC commands (/learn, /hicortex-activate) if present
1249
+ cleanupLegacyCcCommands();
1299
1250
  // Setup Hermes if detected
1300
1251
  if (d.hermesFound) {
1301
1252
  // localhost bypass makes the token optional for co-located installs;
@@ -1325,13 +1276,15 @@ async function runInit(options = {}) {
1325
1276
  // failures are swallowed inside sendLifecycleEvent.
1326
1277
  await (0, telemetry_js_1.sendLifecycleEvent)("install", HICORTEX_HOME, readHomeConfig(HICORTEX_HOME), pkgVersion());
1327
1278
  console.log("Next steps:");
1328
- console.log(" 1. Restart Claude Code to pick up the new MCP server and SessionStart hook");
1279
+ // Counter-based so the list stays contiguous (1,2,3,4) whether or not Hermes
1280
+ // was detected — a conditional middle step used to leave a "1, 3, 4" gap.
1281
+ let step = 1;
1282
+ console.log(` ${step++}. Restart Claude Code to pick up the new MCP server and SessionStart hook`);
1329
1283
  if (d.hermesFound) {
1330
- console.log(" 2. Activate the Hermes plugin: run `hermes memory setup`, select 'hicortex', then restart the gateway(s)");
1284
+ console.log(` ${step++}. Activate the Hermes plugin: run \`hermes memory setup\`, select 'hicortex', then restart the gateway(s)`);
1331
1285
  }
1332
- console.log(" 3. Ask your agent: 'What Hicortex tools do you have?'");
1333
- console.log(" 4. Try /learn to save something to long-term memory");
1334
- console.log(` 5. Check server: curl ${serverUrl}/health`);
1286
+ console.log(` ${step++}. Ask your agent: 'What Hicortex tools do you have?'`);
1287
+ console.log(` ${step++}. Check server: curl ${serverUrl}/health`);
1335
1288
  }
1336
1289
  // ---------------------------------------------------------------------------
1337
1290
  // Client Mode Init
@@ -1470,8 +1423,8 @@ async function runClientInit(serverUrl, agentName) {
1470
1423
  registerCcMcp(serverUrl);
1471
1424
  }
1472
1425
  allowHicortexTools();
1473
- // Step 5: Install CC commands
1474
- installCcCommands();
1426
+ // Step 5: Remove legacy pre-0.10 CC commands if present
1427
+ cleanupLegacyCcCommands();
1475
1428
  // Step 6: Install SessionStart hook for query-time lessons + the #192
1476
1429
  // per-prompt pushed-recall hooks.
1477
1430
  installSessionStartHook();
package/dist/llm.d.ts CHANGED
@@ -191,8 +191,10 @@ export declare class RateLimitError extends Error {
191
191
  }
192
192
  export declare class LlmClient {
193
193
  private config;
194
- private rateLimitedUntil;
195
194
  constructor(config: LlmConfig);
195
+ /** Endpoint identity for shared rate-limit state (provider + base URL). */
196
+ private get endpointKey();
197
+ private get rateLimitedUntil();
196
198
  /** Check if we're currently rate limited */
197
199
  get isRateLimited(): boolean;
198
200
  private handleRateLimit;
package/dist/llm.js CHANGED
@@ -466,12 +466,25 @@ class RateLimitError extends Error {
466
466
  }
467
467
  }
468
468
  exports.RateLimitError = RateLimitError;
469
+ // Rate-limit backoff is shared across all LlmClient instances that target the
470
+ // same endpoint, keyed by provider@baseUrl. completeWithOverride() spins up a
471
+ // throwaway client per call for the distill/reflect/classify override tiers;
472
+ // with per-instance state each throwaway started un-rate-limited and re-hit a
473
+ // 429'd provider immediately, defeating the backoff on exactly the configs
474
+ // (e.g. z.ai via distillBaseUrl/reflectBaseUrl) that route through those tiers.
475
+ const rateLimitedUntilByEndpoint = new Map();
469
476
  class LlmClient {
470
477
  config;
471
- rateLimitedUntil = 0;
472
478
  constructor(config) {
473
479
  this.config = config;
474
480
  }
481
+ /** Endpoint identity for shared rate-limit state (provider + base URL). */
482
+ get endpointKey() {
483
+ return `${this.config.provider}@${this.config.baseUrl ?? ""}`;
484
+ }
485
+ get rateLimitedUntil() {
486
+ return rateLimitedUntilByEndpoint.get(this.endpointKey) ?? 0;
487
+ }
475
488
  /** Check if we're currently rate limited */
476
489
  get isRateLimited() {
477
490
  return Date.now() < this.rateLimitedUntil;
@@ -482,9 +495,10 @@ class LlmClient {
482
495
  const retryMs = retryAfter
483
496
  ? parseInt(retryAfter, 10) * 1000
484
497
  : DEFAULT_RATE_LIMIT_RETRY_MS;
485
- this.rateLimitedUntil = Date.now() + retryMs;
486
- console.log(`[hicortex] Rate limited by LLM provider. ` +
487
- `Will retry after ${new Date(this.rateLimitedUntil).toISOString()}`);
498
+ const until = Date.now() + retryMs;
499
+ rateLimitedUntilByEndpoint.set(this.endpointKey, until);
500
+ console.log(`[hicortex] Rate limited by LLM provider (${this.endpointKey}). ` +
501
+ `Will retry after ${new Date(until).toISOString()}`);
488
502
  throw new RateLimitError(retryMs);
489
503
  }
490
504
  /**
@@ -128,27 +128,18 @@ function createMcpServer() {
128
128
  }
129
129
  });
130
130
  // -- hicortex_get --
131
- 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. When the memory shapes your answer, cite it to the user (id + date + origin agent).", {
131
+ 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 fetch entries that could change your action — not every shown one. When the memory shapes your answer, cite it to the user (id + date + origin agent) — mark a fetched memory `FETCHED` and a one-line entry cited unread `SNIPPET`; don't pass SNIPPET off as established.", {
132
132
  id: zod_1.z.string().describe("Memory id (as shown in recall index/search results)"),
133
133
  }, async ({ id }) => {
134
134
  if (!db)
135
135
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
136
+ // Delegates to formatMemoryGetText → handleMemoryGet, so the citation
137
+ // (incl. the #204 FETCHED marker) is built in ONE place shared with the
138
+ // REST GET /memory path. CC reaches Hicortex through THIS MCP tool;
139
+ // before #207's fix it got a marker-less citation built inline here.
136
140
  try {
137
- // Prefix ids resolve (F6): citations show the 8-char id, so this tool
138
- // must accept it like /update and /delete do.
139
- const fullId = storage.resolveMemoryId(db, id);
140
- const mem = fullId ? storage.getMemory(db, fullId) : null;
141
- if (!mem)
142
- return { content: [{ type: "text", text: `No memory with id ${id}` }], isError: true };
143
- // Real use → full strengthen (access_count + hardening + prune shield).
144
- storage.strengthenMemory(db, mem.id, new Date().toISOString());
145
- // Provenance header (built-in citing norm, 0.14.1): id, type, project,
146
- // ORIGIN AGENT (shared brain — the memory may come from another
147
- // agent's session), and date, plus the explicit citation instruction.
148
- const date = (mem.created_at ?? "").slice(0, 10);
149
- const header = `[memory ${mem.id} | ${mem.memory_type ?? "episode"} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
150
- `Cite as (memory ${String(mem.id).slice(0, 8)}, ${date}) where this shapes your answer; it may be stale — newer memories supersede older.`;
151
- return { content: [{ type: "text", text: `${header}\n\n${mem.content ?? ""}` }] };
141
+ const r = (0, recall_index_js_1.formatMemoryGetText)(db, { id });
142
+ return { content: [{ type: "text", text: r.text }], isError: r.status !== 200 };
152
143
  }
153
144
  catch (err) {
154
145
  return { content: [{ type: "text", text: `Get failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
@@ -488,11 +479,14 @@ async function startServer(options = {}) {
488
479
  retrieval.configureDecay({ halfLifeDays: savedConfig?.decayHalfLifeDays });
489
480
  const recallCfg = retrieval.configureRecall(savedConfig);
490
481
  const scoringCfg = retrieval.configureScoring(savedConfig);
482
+ const sessionIntentCfg = retrieval.configureSessionIntent(savedConfig);
491
483
  console.log(`[hicortex] Recall: k=${recallCfg.searchLimit}/recent=${recallCfg.recentLimit}` +
492
484
  `/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots} · ` +
493
485
  `score sim=${scoringCfg.similarity}/str=${scoringCfg.strength}/conn=${scoringCfg.connections}` +
494
486
  `/rec=${scoringCfg.recency}, fresh=${scoringCfg.freshnessBoostWeight}@${scoringCfg.freshnessBoostDays}d, ` +
495
- `superseded×${scoringCfg.supersededDemotion}`);
487
+ `superseded×${scoringCfg.supersededDemotion}` +
488
+ `, intent w=${sessionIntentCfg.weight}` +
489
+ (sessionIntentCfg.weight === 0 ? " (disabled)" : ""));
496
490
  recallRegistry = new recall_registry_js_1.SessionRecallRegistry({
497
491
  reshowTurns: savedConfig?.recallReshowTurns,
498
492
  });
@@ -510,14 +504,24 @@ async function startServer(options = {}) {
510
504
  const app = (0, express_1.default)();
511
505
  // Raise the body limit — whole-session denoised transcripts exceed the 100 kB default.
512
506
  app.use(express_1.default.json({ limit: "25mb" }));
513
- // CORS: must be before auth so preflight OPTIONS requests get proper headers
507
+ // CORS: reflect ONLY explicitly-allowlisted origins (config.corsAllowedOrigins),
508
+ // and never send Access-Control-Allow-Credentials. Reflecting any origin with
509
+ // credentials — combined with the localhost auth bypass and the default 0.0.0.0
510
+ // bind — let any web page the user visits read/mutate the memory store via
511
+ // fetch() to localhost with no token (the browser connects from 127.0.0.1, so
512
+ // the bypass grants access, and the reflected Allow-Origin let the page read the
513
+ // response). The bundled UIs (/viz, /context/ui) are same-origin and need no CORS
514
+ // headers at all; cross-origin access is opt-in per the hosted plan (#110 §2).
515
+ // Must run before auth so allowlisted preflight OPTIONS get their headers.
516
+ const corsAllowedOrigins = Array.isArray(savedConfig?.corsAllowedOrigins)
517
+ ? savedConfig.corsAllowedOrigins.filter((o) => typeof o === "string")
518
+ : [];
514
519
  app.use((req, res, next) => {
515
520
  const origin = req.headers.origin;
516
- if (origin) {
521
+ if (origin && corsAllowedOrigins.includes(origin)) {
517
522
  res.setHeader("Access-Control-Allow-Origin", origin);
518
523
  res.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST, OPTIONS");
519
524
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
520
- res.setHeader("Access-Control-Allow-Credentials", "true");
521
525
  res.setHeader("Vary", "Origin");
522
526
  }
523
527
  if (req.method === "OPTIONS") {
@@ -668,12 +672,32 @@ async function startServer(options = {}) {
668
672
  registry: recallRegistry,
669
673
  // Client-pushed project/privacy scoping (F1) rides through to
670
674
  // retrieval, which handles the filtered over-fetch itself.
671
- retrieveFn: (query, limit, filters) => retrieval.retrieve(db, embedder_js_1.embed, query, {
672
- limit,
673
- noStrengthen: true,
674
- project: filters?.project,
675
- privacy: filters?.privacy,
676
- }),
675
+ // #192 session-intent keying (0.15.3): embed the prompt ONCE here,
676
+ // blend with the session's rolling centroid, and pass the blended
677
+ // vector to retrieve() via queryEmbedding so retrieve() does NOT
678
+ // re-embed. Turn 1 (no centroid yet) and weight=0 both reduce to a
679
+ // pure-prompt search (the kill-switch). The centroid is updated AFTER
680
+ // reading the prior one — so turn 1 searches with pure prompt, then
681
+ // seeds the centroid for turn 2+ to blend against.
682
+ retrieveFn: async (query, limit, filters, sessionId) => {
683
+ const { weight, alpha } = retrieval.getSessionIntent();
684
+ const promptEmb = await (0, embedder_js_1.embed)(query);
685
+ // weight=0 (kill-switch): the centroid is neither read nor written.
686
+ const centroid = weight > 0 ? recallRegistry.getCentroid(sessionId) : undefined;
687
+ const queryVec = retrieval.blendQueryVector(promptEmb, centroid, weight);
688
+ if (weight > 0)
689
+ recallRegistry.updateCentroid(sessionId, promptEmb, alpha);
690
+ return retrieval.retrieve(db, embedder_js_1.embed, query, {
691
+ limit,
692
+ noStrengthen: true,
693
+ // #203: project + mission_domains are SOFT affinity (zero-boost
694
+ // neutral), threaded into computeScore. privacy stays a hard filter.
695
+ project: filters?.project,
696
+ privacy: filters?.privacy,
697
+ missionDomains: filters?.mission_domains,
698
+ queryEmbedding: queryVec,
699
+ });
700
+ },
677
701
  options: recallIndexOptions,
678
702
  }, req.body);
679
703
  res.status(r.status).json(r.body);
@@ -31,9 +31,9 @@ exports.MEMORY_SECTION_NAME = "memory";
31
31
  function renderMemoryInstructions() {
32
32
  return [
33
33
  "Your long-term memory is Hicortex — shared across all agents and sessions.",
34
- "- A `## Memory recall (auto)` index may arrive with prompts: it is a MENU, not content. Fetch a full memory with `hicortex_get(id)` only when the entry is relevant to your current task.",
34
+ "- A `## Memory recall (auto)` index may arrive with prompts: it is a MENU, not content. Fetch a full memory with `hicortex_get(id)` when the entry could change how you handle the current task.",
35
35
  "- Recall before assuming: `hicortex_search` for prior decisions/facts/preferences, `hicortex_recent` to catch up on a project.",
36
- "- Cite any memory you rely on (id, date); on conflicts, newer memories supersede older.",
36
+ "- Cite any memory you rely on by id + date, and mark it `FETCHED` (you read the full memory via `hicortex_get`) or `SNIPPET` (the one-line entry only). Don't present a SNIPPET citation as established. On conflicts, newer memories supersede older.",
37
37
  "- Capture is automatic (nightly). Do not manually ingest routine content — `hicortex_ingest` is for explicitly requested learnings only.",
38
38
  "- Never inspect, test, or modify memory/plugin/gateway infrastructure (configs, services, tokens). If a memory tool seems missing or broken, say so and stop.",
39
39
  ].join("\n");
@@ -23,6 +23,6 @@ interface HookPayload {
23
23
  * there is nothing to send (no session id, or an unhandled event). Exported
24
24
  * for tests.
25
25
  */
26
- export declare function buildHookRequest(payload: HookPayload): Record<string, unknown> | null;
26
+ export declare function buildHookRequest(payload: HookPayload, cwd?: string): Record<string, unknown> | null;
27
27
  export declare function runRecallHook(): Promise<void>;
28
28
  export {};
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.buildHookRequest = buildHookRequest;
18
18
  exports.runRecallHook = runRecallHook;
19
19
  const lessons_context_js_1 = require("./lessons-context.js");
20
+ const node_path_1 = require("node:path");
20
21
  const FETCH_TIMEOUT_MS = 1000;
21
22
  /** Read all of stdin (CC pipes the hook payload JSON). */
22
23
  async function readStdin() {
@@ -31,7 +32,7 @@ async function readStdin() {
31
32
  * there is nothing to send (no session id, or an unhandled event). Exported
32
33
  * for tests.
33
34
  */
34
- function buildHookRequest(payload) {
35
+ function buildHookRequest(payload, cwd = process.cwd()) {
35
36
  const sessionId = typeof payload.session_id === "string" && payload.session_id
36
37
  ? payload.session_id
37
38
  : null;
@@ -43,7 +44,11 @@ function buildHookRequest(payload) {
43
44
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
44
45
  if (!prompt)
45
46
  return null;
46
- return { session_id: sessionId, prompt };
47
+ // #203 scope: derive project from the session cwd so retrieval can apply a
48
+ // soft project-affinity boost. basename(cwd) matches capture's
49
+ // decodeProjectDirName for non-hyphenated dirs (the common case); a hyphen
50
+ // edge case is a pre-existing capture bug, filed separately.
51
+ return { session_id: sessionId, prompt, project: (0, node_path_1.basename)(cwd) };
47
52
  }
48
53
  async function runRecallHook() {
49
54
  const cfg = (0, lessons_context_js_1.resolveConfig)();
@@ -44,20 +44,37 @@ export declare function passesRelevanceGate(r: MemorySearchResult, minSimilarity
44
44
  /** Recall filters a client may push per request (#193 review F1): a scoped
45
45
  * plugin (Hermes privacy_filter / default_project) must be able to narrow
46
46
  * recall exactly like the legacy /search prefetch did — dropping them
47
- * silently would leak out-of-scope memory titles into the injected index. */
47
+ * silently would leak out-of-scope memory titles into the injected index.
48
+ *
49
+ * #203: `project` and `mission_domains` are now SOFT affinity signals in
50
+ * retrieval (zero-boost neutral, never a filter / penalty); `privacy` stays a
51
+ * hard filter (security boundary). They ride the body → retrieveFn →
52
+ * retrieve() → computeScore path unchanged in shape. */
48
53
  export interface RecallFilters {
49
54
  project?: string;
50
55
  privacy?: string[];
56
+ /** #203: Hermes mission domains (declared in plugin config). Soft domain
57
+ * affinity in computeScore via max overlapping memory_tags.weight. */
58
+ mission_domains?: string[];
51
59
  }
52
60
  export interface RecallIndexDeps {
53
61
  db: Database.Database;
54
62
  registry: SessionRecallRegistry;
55
- retrieveFn: (query: string, limit: number, filters?: RecallFilters) => Promise<MemorySearchResult[]>;
63
+ /** Search closure. `sessionId` is forwarded so the closure (in mcp-server)
64
+ * can resolve/update the session-intent centroid and pass a blended query
65
+ * vector into retrieve() — see #192 session-intent keying (0.15.3). */
66
+ retrieveFn: (query: string, limit: number, filters: RecallFilters | undefined, sessionId: string) => Promise<MemorySearchResult[]>;
56
67
  options?: RecallIndexOptions;
57
68
  }
69
+ /** Normalize a request-supplied string-list param: array of strings or a CSV
70
+ * string → string[] | undefined. Anything else (or an empty result) means
71
+ * "absent" — never a partial guess. Shared by `parsePrivacyParam` and
72
+ * `mission_domains` (#203) so both accept `["A","B"]` and `"A, B"` alike. */
73
+ export declare function parseStringListParam(v: unknown): string[] | undefined;
58
74
  /** Normalize a request-supplied privacy filter: array of strings or a CSV
59
75
  * string → string[] | undefined. Anything else (or an empty result) means
60
- * "no filter" — never a partial guess. */
76
+ * "no filter" — never a partial guess. Delegates to parseStringListParam;
77
+ * kept as a named export for tests and handleMemoryGet callers. */
61
78
  export declare function parsePrivacyParam(v: unknown): string[] | undefined;
62
79
  /**
63
80
  * Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
@@ -80,3 +97,20 @@ export declare function handleMemoryGet(db: Database.Database, query: {
80
97
  id?: unknown;
81
98
  privacy?: unknown;
82
99
  }): RecallIndexResult;
100
+ /**
101
+ * MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
102
+ * text block the MCP tool returns (provenance header + the SHARED citation +
103
+ * content). Extracted from the MCP tool handler so its output — incl. the
104
+ * #204 FETCHED marker, which rides on handleMemoryGet's citation — is unit-
105
+ * testable. The citation string is built ONCE (handleMemoryGet); this only
106
+ * frames it, mirroring how /recall-index is shared across harnesses. The
107
+ * extraction closes the #207 gap (CC's MCP path had a marker-less citation
108
+ * built inline, while the REST path used handleMemoryGet — same contract, two
109
+ * implementations, one updated).
110
+ */
111
+ export declare function formatMemoryGetText(db: Database.Database, query: {
112
+ id?: unknown;
113
+ }): {
114
+ status: number;
115
+ text: string;
116
+ };