@gamaze/hicortex 0.15.3 → 0.16.1

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.
@@ -25,13 +25,31 @@ import { SessionRecallRegistry } from "./recall-registry.js";
25
25
  export interface RecallIndexOptions {
26
26
  /** Minimum measured cosine for vector-only candidates (config
27
27
  * `recallMinSimilarity`). FTS-matched candidates pass regardless — a BM25
28
- * text match is direct evidence of relevance. Default 0.55 (the neutral
29
- * placeholder similarity is 0.5; anything at/below that is noise). */
28
+ * text match is direct evidence of relevance. Default 0.62 (raised from 0.55
29
+ * on 2026-08-03 per a 0.01-step floor sweep on the rewritten corpus): steady
30
+ * ~3:1 noise:signal removal with no knee; 0.62 = +2.2pts precision, 10/98
31
+ * prompts silent, sits below the 0.63 local pessimum. The floor is a noise
32
+ * dial, NOT a silence mechanism — at 0.62 each correctly-silenced empty prompt
33
+ * comes with ~1.5 wrongly-silenced (real signal); a non-cosine gate is the
34
+ * real silence fix (eval #3 §4). */
30
35
  minSimilarity?: number;
31
- /** Max index lines per response (config `recallMaxItems`). Default 6. */
36
+ /** Max index lines per response (config `recallMaxItems`). Default 5
37
+ * (lowered from 6 on 2026-08-03). Per-slot decomposition at floor 0.62:
38
+ * slot 6 gives NO prompt its first relevant memory — "6 is wrong" is the
39
+ * robust, prompt-set-independent finding, and 5 captures it. The K-sweep
40
+ * is monotone (precision@4 33.7% > @6 30.6% > @8 28.3%), so 4 is
41
+ * lower-noise — but the 4-vs-5 distinction rests on 5 of 98 prompts and is
42
+ * overfitting-fragile (K and the floor were tuned on the same set); 5 hedges
43
+ * with coverage at modest cost. Lower to 4 if a fresh-prompt eval replicates. */
32
44
  maxItems?: number;
33
45
  /** Prompts shorter than this are skipped (continuations, "yes", "do it"). */
34
46
  minPromptLength?: number;
47
+ /** Max chars of the memory's first line shown in an index entry (config
48
+ * `recallTitleChars`). Default 100 (reverted from 150 on 2026-08-03): the
49
+ * full-corpus relevance eval (#3, §5) found 100 vs 150 statistically
50
+ * identical (0.6pts apart, N=40, full CI overlap); 100 saves ~13% tokens
51
+ * per block. */
52
+ titleChars?: number;
35
53
  }
36
54
  export interface RecallIndexResult {
37
55
  status: number;
@@ -39,15 +57,44 @@ export interface RecallIndexResult {
39
57
  }
40
58
  /** First content line, de-markdowned and truncated — the index line title. */
41
59
  export declare function memoryTitle(content: string, maxLen?: number): string;
42
- /** Relevance gate: real text match, or measured cosine above the floor. */
60
+ /**
61
+ * Render one production index line. Exported (2026-08-02, relevance eval #v2)
62
+ * so the eval can measure the REAL rendered surface instead of reimplementing
63
+ * it — `maxLen` threads through to `memoryTitle` unchanged (default
64
+ * DEFAULT_TITLE_CHARS = 100, config `recallTitleChars`) so the eval's snippet-length
65
+ * sweep (spec §4.2) can call this SAME function at 100/150/title1sent without
66
+ * duplicating the date/scope/agent/type meta-line logic.
67
+ */
68
+ export declare function formatIndexLine(r: MemorySearchResult & {
69
+ domain?: string | null;
70
+ }, maxLen?: number): string;
71
+ /**
72
+ * Relevance gate: a real BM25 text match (FTS) passes unconditionally; a
73
+ * vector-only candidate must clear `minSimilarity`.
74
+ *
75
+ * NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
76
+ * weight toward FTS-sourced entries. In practice FTS is currently inert on
77
+ * real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
78
+ * a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
79
+ * is safe as measured. But FTS quality is unmeasured; if FTS starts firing
80
+ * (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
81
+ */
43
82
  export declare function passesRelevanceGate(r: MemorySearchResult, minSimilarity: number): boolean;
44
83
  /** Recall filters a client may push per request (#193 review F1): a scoped
45
84
  * plugin (Hermes privacy_filter / default_project) must be able to narrow
46
85
  * recall exactly like the legacy /search prefetch did — dropping them
47
- * silently would leak out-of-scope memory titles into the injected index. */
86
+ * silently would leak out-of-scope memory titles into the injected index.
87
+ *
88
+ * #203: `project` and `mission_domains` are now SOFT affinity signals in
89
+ * retrieval (zero-boost neutral, never a filter / penalty); `privacy` stays a
90
+ * hard filter (security boundary). They ride the body → retrieveFn →
91
+ * retrieve() → computeScore path unchanged in shape. */
48
92
  export interface RecallFilters {
49
93
  project?: string;
50
94
  privacy?: string[];
95
+ /** #203: Hermes mission domains (declared in plugin config). Soft domain
96
+ * affinity in computeScore via max overlapping memory_tags.weight. */
97
+ mission_domains?: string[];
51
98
  }
52
99
  export interface RecallIndexDeps {
53
100
  db: Database.Database;
@@ -58,9 +105,15 @@ export interface RecallIndexDeps {
58
105
  retrieveFn: (query: string, limit: number, filters: RecallFilters | undefined, sessionId: string) => Promise<MemorySearchResult[]>;
59
106
  options?: RecallIndexOptions;
60
107
  }
108
+ /** Normalize a request-supplied string-list param: array of strings or a CSV
109
+ * string → string[] | undefined. Anything else (or an empty result) means
110
+ * "absent" — never a partial guess. Shared by `parsePrivacyParam` and
111
+ * `mission_domains` (#203) so both accept `["A","B"]` and `"A, B"` alike. */
112
+ export declare function parseStringListParam(v: unknown): string[] | undefined;
61
113
  /** Normalize a request-supplied privacy filter: array of strings or a CSV
62
114
  * string → string[] | undefined. Anything else (or an empty result) means
63
- * "no filter" — never a partial guess. */
115
+ * "no filter" — never a partial guess. Delegates to parseStringListParam;
116
+ * kept as a named export for tests and handleMemoryGet callers. */
64
117
  export declare function parsePrivacyParam(v: unknown): string[] | undefined;
65
118
  /**
66
119
  * Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
@@ -83,3 +136,20 @@ export declare function handleMemoryGet(db: Database.Database, query: {
83
136
  id?: unknown;
84
137
  privacy?: unknown;
85
138
  }): RecallIndexResult;
139
+ /**
140
+ * MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
141
+ * text block the MCP tool returns (provenance header + the SHARED citation +
142
+ * content). Extracted from the MCP tool handler so its output — incl. the
143
+ * #204 FETCHED marker, which rides on handleMemoryGet's citation — is unit-
144
+ * testable. The citation string is built ONCE (handleMemoryGet); this only
145
+ * frames it, mirroring how /recall-index is shared across harnesses. The
146
+ * extraction closes the #207 gap (CC's MCP path had a marker-less citation
147
+ * built inline, while the REST path used handleMemoryGet — same contract, two
148
+ * implementations, one updated).
149
+ */
150
+ export declare function formatMemoryGetText(db: Database.Database, query: {
151
+ id?: unknown;
152
+ }): {
153
+ status: number;
154
+ text: string;
155
+ };
@@ -55,18 +55,33 @@ var __importStar = (this && this.__importStar) || (function () {
55
55
  })();
56
56
  Object.defineProperty(exports, "__esModule", { value: true });
57
57
  exports.memoryTitle = memoryTitle;
58
+ exports.formatIndexLine = formatIndexLine;
58
59
  exports.passesRelevanceGate = passesRelevanceGate;
60
+ exports.parseStringListParam = parseStringListParam;
59
61
  exports.parsePrivacyParam = parsePrivacyParam;
60
62
  exports.handleRecallIndex = handleRecallIndex;
61
63
  exports.handleMemoryGet = handleMemoryGet;
64
+ exports.formatMemoryGetText = formatMemoryGetText;
62
65
  const storage = __importStar(require("./storage.js"));
63
- const DEFAULT_MIN_SIMILARITY = 0.55;
64
- const DEFAULT_MAX_ITEMS = 6;
66
+ /** Relevance-gate floor for vector-only candidates (config `recallMinSimilarity`).
67
+ * 0.62 (was 0.55; raised 2026-08-03 on the fine-grain floor sweep — see the
68
+ * minSimilarity doc above). */
69
+ const DEFAULT_MIN_SIMILARITY = 0.62;
70
+ /** Max index lines per pushed recall block (config `recallMaxItems`).
71
+ * 5 (was 6; lowered 2026-08-03 — slot 6 is pure padding at floor 0.62). */
72
+ const DEFAULT_MAX_ITEMS = 5;
65
73
  const DEFAULT_MIN_PROMPT_LENGTH = 20;
66
- /** Retrieve more than maxItems so gating + dedup still leave a full menu. */
74
+ /** Default index-line title length. 100 (reverted from 150 on 2026-08-03:
75
+ * eval #3 §5 showed 100 vs 150 statistically identical; 100 saves ~13% tokens). */
76
+ const DEFAULT_TITLE_CHARS = 100;
77
+ /** Over-fetch multiplier: retrieve `maxItems × 3` candidates so gating + dedup
78
+ * still leave a full menu. Kept at 3 after maxItems 6→5 and the higher floor —
79
+ * permit-short is intended (returning fewer than maxItems when fewer clear the
80
+ * gate is correct, not a defect); raise only if blocks are persistently
81
+ * under-filled in production. */
67
82
  const CANDIDATE_MULTIPLIER = 3;
68
83
  /** First content line, de-markdowned and truncated — the index line title. */
69
- function memoryTitle(content, maxLen = 100) {
84
+ function memoryTitle(content, maxLen = DEFAULT_TITLE_CHARS) {
70
85
  const firstLine = content
71
86
  .split("\n")
72
87
  .map((l) => l.trim())
@@ -86,22 +101,49 @@ function formatDate(iso) {
86
101
  const mm = String(d.getMonth() + 1).padStart(2, "0");
87
102
  return `${dd}.${mm}.${d.getFullYear()}`;
88
103
  }
89
- function formatIndexLine(r) {
90
- const meta = [formatDate(r.created_at), r.domain ?? r.project ?? undefined, r.memory_type]
104
+ /**
105
+ * Render one production index line. Exported (2026-08-02, relevance eval #v2)
106
+ * so the eval can measure the REAL rendered surface instead of reimplementing
107
+ * it — `maxLen` threads through to `memoryTitle` unchanged (default
108
+ * DEFAULT_TITLE_CHARS = 100, config `recallTitleChars`) so the eval's snippet-length
109
+ * sweep (spec §4.2) can call this SAME function at 100/150/title1sent without
110
+ * duplicating the date/scope/agent/type meta-line logic.
111
+ */
112
+ function formatIndexLine(r, maxLen = DEFAULT_TITLE_CHARS) {
113
+ // Provenance (#202): date, scope (domain else project), ORIGIN AGENT, type.
114
+ // The origin agent lets a reader calibrate trust — "from my session" vs
115
+ // another agent/project — before fetching or acting on an entry.
116
+ const meta = [
117
+ formatDate(r.created_at),
118
+ r.domain ?? r.project ?? undefined,
119
+ r.source_agent ?? undefined,
120
+ r.memory_type,
121
+ ]
91
122
  .filter(Boolean)
92
123
  .join(", ");
93
- return `- [${r.id}] ${memoryTitle(r.content)}${meta ? ` (${meta})` : ""}`;
124
+ return `- [${r.id}] ${memoryTitle(r.content, maxLen)}${meta ? ` (${meta})` : ""}`;
94
125
  }
95
- /** Relevance gate: real text match, or measured cosine above the floor. */
126
+ /**
127
+ * Relevance gate: a real BM25 text match (FTS) passes unconditionally; a
128
+ * vector-only candidate must clear `minSimilarity`.
129
+ *
130
+ * NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
131
+ * weight toward FTS-sourced entries. In practice FTS is currently inert on
132
+ * real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
133
+ * a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
134
+ * is safe as measured. But FTS quality is unmeasured; if FTS starts firing
135
+ * (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
136
+ */
96
137
  function passesRelevanceGate(r, minSimilarity) {
97
138
  if (r.source === "fts" || r.source === "both")
98
139
  return true;
99
140
  return typeof r.similarity === "number" && r.similarity >= minSimilarity;
100
141
  }
101
- /** Normalize a request-supplied privacy filter: array of strings or a CSV
142
+ /** Normalize a request-supplied string-list param: array of strings or a CSV
102
143
  * string → string[] | undefined. Anything else (or an empty result) means
103
- * "no filter" — never a partial guess. */
104
- function parsePrivacyParam(v) {
144
+ * "absent" — never a partial guess. Shared by `parsePrivacyParam` and
145
+ * `mission_domains` (#203) so both accept `["A","B"]` and `"A, B"` alike. */
146
+ function parseStringListParam(v) {
105
147
  const items = Array.isArray(v)
106
148
  ? v.filter((x) => typeof x === "string")
107
149
  : typeof v === "string"
@@ -110,6 +152,13 @@ function parsePrivacyParam(v) {
110
152
  const cleaned = items.map((s) => s.trim()).filter(Boolean);
111
153
  return cleaned.length > 0 ? cleaned : undefined;
112
154
  }
155
+ /** Normalize a request-supplied privacy filter: array of strings or a CSV
156
+ * string → string[] | undefined. Anything else (or an empty result) means
157
+ * "no filter" — never a partial guess. Delegates to parseStringListParam;
158
+ * kept as a named export for tests and handleMemoryGet callers. */
159
+ function parsePrivacyParam(v) {
160
+ return parseStringListParam(v);
161
+ }
113
162
  /**
114
163
  * Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
115
164
  * all behavior lives here so tests exercise it directly.
@@ -132,13 +181,18 @@ async function handleRecallIndex(deps, body) {
132
181
  return { status: 200, body: { block: null, skipped: "short-prompt" } };
133
182
  }
134
183
  const maxItems = clampInt(deps.options?.maxItems, DEFAULT_MAX_ITEMS, 1, 20);
184
+ const titleChars = clampInt(deps.options?.titleChars, DEFAULT_TITLE_CHARS, 40, 400);
135
185
  const minSimilarity = clampNumber(deps.options?.minSimilarity, DEFAULT_MIN_SIMILARITY, 0, 1);
136
186
  const turn = deps.registry.beginTurn(sessionId);
137
- // Optional client-side scoping (F1): project + privacy ride the body and
138
- // are pushed into retrieval (which handles filtered over-fetch itself).
187
+ // Optional client-side scoping (F1 + #203): project + mission_domains (soft
188
+ // affinity) and privacy (hard filter) ride the body and are pushed into
189
+ // retrieval. project is cwd-derived (CC/OC) or gateway-supplied; mission_domains
190
+ // is Hermes-declared (plugin config). Neither excludes anything — both are
191
+ // zero-boost-neutral score terms in computeScore.
139
192
  const filters = {
140
193
  project: typeof req.project === "string" && req.project ? req.project : undefined,
141
194
  privacy: parsePrivacyParam(req.privacy),
195
+ mission_domains: parseStringListParam(req.mission_domains),
142
196
  };
143
197
  let results;
144
198
  try {
@@ -161,16 +215,18 @@ async function handleRecallIndex(deps, body) {
161
215
  deps.registry.markShown(sessionId, ids);
162
216
  // Exposure signal: shown_count + last_accessed refresh, NOT access_count.
163
217
  storage.touchMemoriesShown(deps.db, ids, new Date().toISOString());
164
- const lines = picked.map((r) => formatIndexLine(r));
218
+ const lines = picked.map((r) => formatIndexLine(r, titleChars));
165
219
  const block = [
166
220
  "## Memory recall (auto)",
167
- // Provenance is BUILT IN, split by function (owner decision 27.07,
168
- // option D): this header carries only the SELECTION-time rules
169
- // supersession (the one moment competing dates are visible side by side)
170
- // and cite-what-you-rely-on (covers snippet-only use, the common case per
171
- // the 0.14.0 field test). The full citation format + origin agent ride on
172
- // the hicortex_get response / GET /memory `citation` field (use-time).
173
- "Possibly relevant memories dates matter, newer supersedes older. Fetch full content with `hicortex_get(id)` when an entry could change your action; cite any memory you rely on (id, date):",
221
+ // Provenance is BUILT IN (owner decision 27.07, option D; extended #202/#204):
222
+ // - #202: origin agent in each one-liner (formatIndexLine) trust calibration.
223
+ // - #204: confidence levels FETCHED (read in full) vs SNIPPET (one-line entry only),
224
+ // so "the agent cited a memory" can no longer pass as "the agent read it".
225
+ // This header carries the SELECTION-time rules: supersession (the one moment
226
+ // competing dates are visible side by side) and cite-with-confidence. The
227
+ // full citation format rides on the hicortex_get response / GET /memory
228
+ // `citation` field (use-time, marked FETCHED).
229
+ "Possibly relevant memories — dates matter, newer supersedes older. Fetch with `hicortex_get(id)` when an entry could change your action. Cite what you rely on by id + date, and mark it `FETCHED` if you read the full memory or `SNIPPET` if you're citing the one-line entry unread — don't pass a SNIPPET citation off as established fact.",
174
230
  ...lines,
175
231
  ].join("\n");
176
232
  return { status: 200, body: { block, shown: ids, turn } };
@@ -212,10 +268,33 @@ function handleMemoryGet(db, query) {
212
268
  status: 200,
213
269
  body: {
214
270
  memory: mem,
215
- citation: `(memory ${String(mem.id).slice(0, 8)}, ${date}, from ${mem.source_agent ?? "unknown"})`,
271
+ citation: `(memory ${String(mem.id).slice(0, 8)}, ${date}, from ${mem.source_agent ?? "unknown"}, FETCHED)`,
216
272
  },
217
273
  };
218
274
  }
275
+ /**
276
+ * MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
277
+ * text block the MCP tool returns (provenance header + the SHARED citation +
278
+ * content). Extracted from the MCP tool handler so its output — incl. the
279
+ * #204 FETCHED marker, which rides on handleMemoryGet's citation — is unit-
280
+ * testable. The citation string is built ONCE (handleMemoryGet); this only
281
+ * frames it, mirroring how /recall-index is shared across harnesses. The
282
+ * extraction closes the #207 gap (CC's MCP path had a marker-less citation
283
+ * built inline, while the REST path used handleMemoryGet — same contract, two
284
+ * implementations, one updated).
285
+ */
286
+ function formatMemoryGetText(db, query) {
287
+ const r = handleMemoryGet(db, query);
288
+ if (r.status !== 200) {
289
+ return { status: r.status, text: String(r.body.error ?? `No memory with id ${query.id ?? ""}`) };
290
+ }
291
+ const mem = r.body.memory;
292
+ const citation = r.body.citation; // carries FETCHED (#204)
293
+ const date = (mem.created_at ?? "").slice(0, 10);
294
+ const header = `[memory ${mem.id} | ${mem.memory_type ?? "episode"} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
295
+ `Cite as ${citation} where this shapes your answer; it may be stale — newer memories supersede older.`;
296
+ return { status: 200, text: `${header}\n\n${mem.content ?? ""}` };
297
+ }
219
298
  function clampInt(v, dflt, min, max) {
220
299
  const n = Number(v);
221
300
  if (!Number.isFinite(n))
@@ -60,12 +60,28 @@ interface ScoringWeights {
60
60
  freshnessBoostDays: number;
61
61
  freshnessBoostWeight: number;
62
62
  supersededDemotion: number;
63
+ /** #203 soft affinity boost on exact project match. */
64
+ projectAffinity: number;
65
+ /** #203 soft affinity boost multiplier on max overlapping domain-tag weight. */
66
+ domainAffinity: number;
67
+ /** #205 RRF k parameter (1/(k+rank+1)). Larger ⇒ shallower rank curve. */
68
+ rrfK: number;
69
+ /** #205 composite-score share of the final blend (RRF gets the remainder). */
70
+ rrfCompositeWeight: number;
71
+ /** #205 per-list RRF weight for the FTS list (BM25-driven candidates). */
72
+ rrfFtsWeight: number;
73
+ /** #205 per-list RRF weight for the vector list (KNN-driven candidates). */
74
+ rrfVectorWeight: number;
63
75
  }
64
76
  /**
65
77
  * Configure scoring weights + ranking knobs from config. Called at boot by the
66
78
  * server and the nightly (alongside configureDecay/configureRecall) so
67
79
  * retrieval and consolidation rank identically. Invalid/absent values keep the
68
- * shipped default per key. Returns the resolved set for logging/tests.
80
+ * shipped default per key. Returns the resolved set for logging/tests. Also
81
+ * pushes the #205 BM25F field weights into storage (storage.configureBm25Fts)
82
+ * so searchFts ranks with the same config — BM25F weights live in storage.ts
83
+ * (next to the FTS column declaration they mirror) but are read here from the
84
+ * SAME config object for one-place tuning.
69
85
  */
70
86
  export declare function configureScoring(config?: Record<string, unknown> | null): ScoringWeights;
71
87
  /** Current resolved weights (tests + status output). */
@@ -127,9 +143,31 @@ export declare function effectiveStrength(baseStrength: number, lastAccessed: st
127
143
  /**
128
144
  * Return a composite relevance score in [0, 1] for a candidate memory.
129
145
  * Exported for exact-value tests of the similarity component (#145).
146
+ *
147
+ * #203 soft affinity (options.scope + options.tagWeights): two additive,
148
+ * graded, zero-boost-neutral terms — project affinity (exact project match)
149
+ * and domain affinity (max overlapping memory_tags.weight × scope). Both are
150
+ * 0 when the scope is absent (byte-identical to pre-#203) and NEVER negative
151
+ * (a foreign memory adds 0, never a penalty — penalties re-introduce
152
+ * soft-exclusion). See `AffinityScope`.
130
153
  */
154
+ export interface AffinityScope {
155
+ /** Exact-match project from the client (CC/OC cwd-derived; /search project). */
156
+ project?: string | null;
157
+ /** Hermes mission domains declared in plugin config. Drawn from the same
158
+ * vocabulary as memory_tags (config `domains`). */
159
+ missionDomains?: string[];
160
+ }
131
161
  export declare function computeScore(memory: Memory, distance: number, connectionCount: number, maxConnections: number, now: Date, options?: {
132
162
  superseded?: boolean;
163
+ /** #203: when present, project/domain affinity boosts are applied. */
164
+ scope?: AffinityScope;
165
+ /** Candidate's graded domain tags (memory_tags rows). Loaded batched for
166
+ * the whole candidate set in retrieve(); used for domain affinity. */
167
+ tagWeights?: Array<{
168
+ tag: string;
169
+ weight: number | null;
170
+ }>;
133
171
  }): number;
134
172
  export interface EmbedFn {
135
173
  (text: string): Promise<Float32Array>;
@@ -137,12 +175,25 @@ export interface EmbedFn {
137
175
  /**
138
176
  * Main retrieval: BM25 + vector search with RRF fusion, graph traversal,
139
177
  * and composite scoring. Strengthens accessed memories.
178
+ *
179
+ * #203 retrieval scoping: `project` and `missionDomains` are SOFT affinity
180
+ * terms in computeScore (zero-boost neutral, never a penalty), NOT filters.
181
+ * `privacy` remains a hard filter (security boundary). `sourceAgent` remains a
182
+ * hard filter (kept for completeness; no production caller currently passes
183
+ * it). When neither project nor missionDomains is sent, scoring is byte-
184
+ * identical to pre-#203 — the kill-switch / no-op guarantee.
140
185
  */
141
186
  export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query: string, options?: {
142
187
  limit?: number;
188
+ /** #203: soft project affinity (exact match boost in computeScore).
189
+ * Formerly a hard WHERE filter (#192); softening removes cross-scope
190
+ * starvation without excluding anything. */
143
191
  project?: string | null;
144
192
  privacy?: string[];
145
193
  sourceAgent?: string;
194
+ /** #203: Hermes mission domains (declared in plugin config). Soft domain
195
+ * affinity in computeScore via max overlapping memory_tags.weight. */
196
+ missionDomains?: string[];
146
197
  /** #192: skip access strengthening — for pushed recall (/recall-index),
147
198
  * where appearing in results must not count as use. */
148
199
  noStrengthen?: boolean;