@gamaze/hicortex 0.16.1 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/capture.d.ts +18 -1
- package/dist/capture.js +3 -2
- package/dist/classify-domains.d.ts +1 -1
- package/dist/classify-domains.js +5 -7
- package/dist/cli.js +10 -2
- package/dist/cluster.d.ts +5 -4
- package/dist/cluster.js +2 -3
- package/dist/consolidate.js +2 -4
- package/dist/db.js +23 -0
- package/dist/dedup.js +1 -1
- package/dist/distiller.js +2 -2
- package/dist/domain-classify.d.ts +1 -1
- package/dist/domain-classify.js +1 -5
- package/dist/eval/run-eval.js +0 -1
- package/dist/index.js +0 -1
- package/dist/init.d.ts +165 -0
- package/dist/init.js +283 -57
- package/dist/mcp-server.js +71 -25
- package/dist/nightly.js +35 -3
- package/dist/nofit.d.ts +1 -1
- package/dist/nofit.js +1 -2
- package/dist/prompts.js +0 -7
- package/dist/recall-index.d.ts +13 -17
- package/dist/recall-index.js +11 -21
- package/dist/retrieval.d.ts +7 -7
- package/dist/retrieval.js +20 -24
- package/dist/schema-prototypes.d.ts +8 -13
- package/dist/schema-prototypes.js +13 -22
- package/dist/seed-lesson.js +0 -1
- package/dist/storage.d.ts +9 -12
- package/dist/storage.js +19 -21
- package/dist/types.d.ts +47 -23
- package/domains.example.json +2 -3
- package/hermes-plugin/hicortex/README.md +3 -1
- package/hermes-plugin/hicortex/config.py +33 -2
- package/hermes-plugin/hicortex/plugin.yaml +1 -1
- package/hermes-plugin/hicortex/provider.py +5 -0
- package/package.json +1 -1
package/dist/nightly.js
CHANGED
|
@@ -71,15 +71,28 @@ const state_js_1 = require("./state.js");
|
|
|
71
71
|
const capture_cursors_js_1 = require("./capture-cursors.js");
|
|
72
72
|
const capture_js_1 = require("./capture.js");
|
|
73
73
|
const telemetry_js_1 = require("./telemetry.js");
|
|
74
|
+
const init_js_1 = require("./init.js");
|
|
74
75
|
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
75
76
|
function readNightlyConfig(stateDir) {
|
|
77
|
+
const configPath = (0, node_path_1.join)(stateDir, "config.json");
|
|
78
|
+
let loaded;
|
|
76
79
|
try {
|
|
77
|
-
|
|
78
|
-
return JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
|
|
80
|
+
loaded = (0, init_js_1.loadConfigStrict)(configPath);
|
|
79
81
|
}
|
|
80
|
-
catch {
|
|
82
|
+
catch (e) {
|
|
83
|
+
// Malformed existing config (bad JSON / non-object / unreadable): visible
|
|
84
|
+
// WARN so the operator fixes it, then fail-soft to null. The strict load
|
|
85
|
+
// also protects the agentId self-heal below — its throw is now reachable
|
|
86
|
+
// here (without this routing, a swallowed parse → null → the `if
|
|
87
|
+
// (savedConfig)` guard would skip the self-heal entirely).
|
|
88
|
+
console.warn(`[hicortex] ${configPath} exists but could not be parsed — running degraded ` +
|
|
89
|
+
`(agentId self-heal and config-driven knobs will not apply this run). ` +
|
|
90
|
+
`Fix the JSON and re-run. Cause: ${e instanceof Error ? e.message : String(e)}`);
|
|
81
91
|
return null;
|
|
82
92
|
}
|
|
93
|
+
// ENOENT → hadFile=false → null (install not set up yet; silent, matches the
|
|
94
|
+
// old catch→null behavior).
|
|
95
|
+
return loaded.hadFile ? loaded.config : null;
|
|
83
96
|
}
|
|
84
97
|
function readConfigLicenseKey(stateDir) {
|
|
85
98
|
try {
|
|
@@ -202,6 +215,17 @@ async function runNightly(options = {}) {
|
|
|
202
215
|
(0, state_js_1.migrateLegacyState)(stateDir);
|
|
203
216
|
// Check mode: client or server
|
|
204
217
|
const savedConfig = readNightlyConfig(stateDir);
|
|
218
|
+
// 0.16.2 activation gap: pre-0.16.2 installs never re-run init, so their
|
|
219
|
+
// config has no agentId → capture sent source_agent_id: null forever (the
|
|
220
|
+
// provenance feature was inert for the whole existing fleet). Self-heal on
|
|
221
|
+
// the first nightly after upgrade: ensureAndPersistAgentId generates + writes
|
|
222
|
+
// the id once (idempotent thereafter). Mutate the in-memory savedConfig so
|
|
223
|
+
// BOTH capture paths (server line below, client via runClientNightly's param)
|
|
224
|
+
// read the value without re-reading the file.
|
|
225
|
+
if (savedConfig) {
|
|
226
|
+
const { agentId } = (0, init_js_1.ensureAndPersistAgentId)((0, node_path_1.join)(stateDir, "config.json"));
|
|
227
|
+
savedConfig.agentId = agentId;
|
|
228
|
+
}
|
|
205
229
|
if (savedConfig?.mode === "client") {
|
|
206
230
|
// --capture-only is accepted in client mode but irrelevant: client nightly
|
|
207
231
|
// is already capture-only (no consolidation step).
|
|
@@ -308,10 +332,14 @@ async function runNightly(options = {}) {
|
|
|
308
332
|
}
|
|
309
333
|
// Step 2: pack each session's delta into ≤60K segments and POST to the
|
|
310
334
|
// local daemon via /distill; cursors advance on confirmed success.
|
|
335
|
+
// source_agent_id / source_domain are per-client provenance from
|
|
336
|
+
// config.json (agentId / sourceDomain) — attribution only, no filtering.
|
|
311
337
|
const result = await (0, capture_js_1.captureBatches)(batches, {
|
|
312
338
|
post: makeLocalPost(port),
|
|
313
339
|
cursorStore,
|
|
314
340
|
dryRun,
|
|
341
|
+
sourceAgentId: savedConfig?.agentId,
|
|
342
|
+
sourceDomain: savedConfig?.sourceDomain,
|
|
315
343
|
});
|
|
316
344
|
memoriesIngested = result.memoriesIngested;
|
|
317
345
|
// A 429/401 stop must hold the watermark too (fix 1): the loop abandoned
|
|
@@ -529,6 +557,10 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
|
|
|
529
557
|
post: makeRemotePost(serverUrl, authToken),
|
|
530
558
|
cursorStore,
|
|
531
559
|
dryRun,
|
|
560
|
+
// Per-client provenance from config.json (agentId / sourceDomain). The
|
|
561
|
+
// server stores these alongside source_agent; nothing filters on them.
|
|
562
|
+
sourceAgentId: config.agentId,
|
|
563
|
+
sourceDomain: config.sourceDomain,
|
|
532
564
|
});
|
|
533
565
|
memoriesIngested = result.memoriesIngested;
|
|
534
566
|
sessionsSent = result.sessionsSent;
|
package/dist/nofit.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ export declare function resolveNoFit(db: Database.Database, memoryId: string, do
|
|
|
90
90
|
* primary derives naturally inside setMemoryTags). Logged distinctly so
|
|
91
91
|
* weak primaries are auditable apart from LLM-tagged rows.
|
|
92
92
|
*/
|
|
93
|
-
export declare function applyWeakPrimary(db: Database.Database, memoryId: string, domain: string, weight: number
|
|
93
|
+
export declare function applyWeakPrimary(db: Database.Database, memoryId: string, domain: string, weight: number): void;
|
|
94
94
|
/**
|
|
95
95
|
* Apply no-association decay to a no-fit-below-floor memory:
|
|
96
96
|
* - clear any leftover memory_tags rows (e.g. a legacy "Unsorted" tag from
|
package/dist/nofit.js
CHANGED
|
@@ -137,10 +137,9 @@ function resolveNoFit(db, memoryId, domains, prototypes, floor) {
|
|
|
137
137
|
* primary derives naturally inside setMemoryTags). Logged distinctly so
|
|
138
138
|
* weak primaries are auditable apart from LLM-tagged rows.
|
|
139
139
|
*/
|
|
140
|
-
function applyWeakPrimary(db, memoryId, domain, weight
|
|
140
|
+
function applyWeakPrimary(db, memoryId, domain, weight) {
|
|
141
141
|
storage.setMemoryTags(db, memoryId, [domain], {
|
|
142
142
|
weights: { [domain]: weight },
|
|
143
|
-
compartments,
|
|
144
143
|
});
|
|
145
144
|
console.log(`[hicortex] weak-primary ${domain} w=${weight.toFixed(2)} for ${memoryId}`);
|
|
146
145
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -105,8 +105,6 @@ EXTRACT into this markdown format:
|
|
|
105
105
|
|
|
106
106
|
# Session Memory: ${date} - ${projectName}
|
|
107
107
|
|
|
108
|
-
## Classification: [pick one: PUBLIC / WORK / PERSONAL / SENSITIVE]
|
|
109
|
-
|
|
110
108
|
### Decisions Made
|
|
111
109
|
- [SUBJECT]: [decision] — [reasoning] (${date})
|
|
112
110
|
|
|
@@ -153,11 +151,6 @@ RULES:
|
|
|
153
151
|
the correction matters deeply. Note the intensity AFTER the subject, never before it
|
|
154
152
|
(e.g. "Pricing tiers: strongly rejected per-agent billing — …", not
|
|
155
153
|
"[Strong Negative] User rejected per-agent billing"). The subject always comes first.
|
|
156
|
-
- PRIVACY CLASSIFICATION (one of):
|
|
157
|
-
- PUBLIC: general tech knowledge, open-source patterns, publicly available info
|
|
158
|
-
- WORK: project-specific decisions, architecture choices, client/business context
|
|
159
|
-
- PERSONAL: personal preferences, family, health, lifestyle, private life
|
|
160
|
-
- SENSITIVE: API keys mentioned, credentials, financial account details, medical records
|
|
161
154
|
- Omit any section that has zero items (don't include empty sections)
|
|
162
155
|
- If nothing worth extracting, output ONLY: "NO_EXTRACT"
|
|
163
156
|
`;
|
package/dist/recall-index.d.ts
CHANGED
|
@@ -81,17 +81,18 @@ export declare function formatIndexLine(r: MemorySearchResult & {
|
|
|
81
81
|
*/
|
|
82
82
|
export declare function passesRelevanceGate(r: MemorySearchResult, minSimilarity: number): boolean;
|
|
83
83
|
/** Recall filters a client may push per request (#193 review F1): a scoped
|
|
84
|
-
* plugin (Hermes
|
|
84
|
+
* plugin (Hermes default_project / mission_domains) must be able to narrow
|
|
85
85
|
* recall exactly like the legacy /search prefetch did — dropping them
|
|
86
86
|
* silently would leak out-of-scope memory titles into the injected index.
|
|
87
87
|
*
|
|
88
|
-
* #203: `project` and `mission_domains` are
|
|
89
|
-
* retrieval (zero-boost neutral, never a filter / penalty)
|
|
90
|
-
*
|
|
91
|
-
*
|
|
88
|
+
* #203: `project` and `mission_domains` are SOFT affinity signals in
|
|
89
|
+
* retrieval (zero-boost neutral, never a filter / penalty). 0.16.x: `privacy`
|
|
90
|
+
* is gone from this shape entirely — the column is vestigial, never filtered,
|
|
91
|
+
* so a plugin's `privacy_filter` is a harmless no-op the server no longer
|
|
92
|
+
* threads through. The body field is still ACCEPTED (backward compat) but
|
|
93
|
+
* ignored. */
|
|
92
94
|
export interface RecallFilters {
|
|
93
95
|
project?: string;
|
|
94
|
-
privacy?: string[];
|
|
95
96
|
/** #203: Hermes mission domains (declared in plugin config). Soft domain
|
|
96
97
|
* affinity in computeScore via max overlapping memory_tags.weight. */
|
|
97
98
|
mission_domains?: string[];
|
|
@@ -107,14 +108,9 @@ export interface RecallIndexDeps {
|
|
|
107
108
|
}
|
|
108
109
|
/** Normalize a request-supplied string-list param: array of strings or a CSV
|
|
109
110
|
* string → string[] | undefined. Anything else (or an empty result) means
|
|
110
|
-
* "absent" — never a partial guess.
|
|
111
|
-
*
|
|
111
|
+
* "absent" — never a partial guess. Used by `mission_domains` (#203) so it
|
|
112
|
+
* accepts `["A","B"]` and `"A, B"` alike. */
|
|
112
113
|
export declare function parseStringListParam(v: unknown): string[] | undefined;
|
|
113
|
-
/** Normalize a request-supplied privacy filter: array of strings or a CSV
|
|
114
|
-
* string → string[] | undefined. Anything else (or an empty result) means
|
|
115
|
-
* "no filter" — never a partial guess. Delegates to parseStringListParam;
|
|
116
|
-
* kept as a named export for tests and handleMemoryGet callers. */
|
|
117
|
-
export declare function parsePrivacyParam(v: unknown): string[] | undefined;
|
|
118
114
|
/**
|
|
119
115
|
* Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
|
|
120
116
|
* all behavior lives here so tests exercise it directly.
|
|
@@ -127,14 +123,14 @@ export declare function handleRecallIndex(deps: RecallIndexDeps, body: unknown):
|
|
|
127
123
|
*
|
|
128
124
|
* - Short/prefix ids resolve via storage.resolveMemoryId (F6) — the 8-char
|
|
129
125
|
* citation ids agents are taught must work here like on /update, /delete.
|
|
130
|
-
* - Optional `privacy` filter (array or CSV): when present and the memory's
|
|
131
|
-
* privacy level is not in the allowed set, respond 404 with the SAME
|
|
132
|
-
* not-found message — a scoped client must not learn the memory exists.
|
|
133
126
|
* - A successful fetch is real use: access_count + 1 (strengthen).
|
|
127
|
+
*
|
|
128
|
+
* 0.16.x: the `privacy` filter gate was removed — the column is vestigial and
|
|
129
|
+
* never filtered. Callers may still send a `privacy` field (backward compat)
|
|
130
|
+
* but it is ignored.
|
|
134
131
|
*/
|
|
135
132
|
export declare function handleMemoryGet(db: Database.Database, query: {
|
|
136
133
|
id?: unknown;
|
|
137
|
-
privacy?: unknown;
|
|
138
134
|
}): RecallIndexResult;
|
|
139
135
|
/**
|
|
140
136
|
* MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
|
package/dist/recall-index.js
CHANGED
|
@@ -58,7 +58,6 @@ exports.memoryTitle = memoryTitle;
|
|
|
58
58
|
exports.formatIndexLine = formatIndexLine;
|
|
59
59
|
exports.passesRelevanceGate = passesRelevanceGate;
|
|
60
60
|
exports.parseStringListParam = parseStringListParam;
|
|
61
|
-
exports.parsePrivacyParam = parsePrivacyParam;
|
|
62
61
|
exports.handleRecallIndex = handleRecallIndex;
|
|
63
62
|
exports.handleMemoryGet = handleMemoryGet;
|
|
64
63
|
exports.formatMemoryGetText = formatMemoryGetText;
|
|
@@ -141,8 +140,8 @@ function passesRelevanceGate(r, minSimilarity) {
|
|
|
141
140
|
}
|
|
142
141
|
/** Normalize a request-supplied string-list param: array of strings or a CSV
|
|
143
142
|
* string → string[] | undefined. Anything else (or an empty result) means
|
|
144
|
-
* "absent" — never a partial guess.
|
|
145
|
-
*
|
|
143
|
+
* "absent" — never a partial guess. Used by `mission_domains` (#203) so it
|
|
144
|
+
* accepts `["A","B"]` and `"A, B"` alike. */
|
|
146
145
|
function parseStringListParam(v) {
|
|
147
146
|
const items = Array.isArray(v)
|
|
148
147
|
? v.filter((x) => typeof x === "string")
|
|
@@ -152,13 +151,6 @@ function parseStringListParam(v) {
|
|
|
152
151
|
const cleaned = items.map((s) => s.trim()).filter(Boolean);
|
|
153
152
|
return cleaned.length > 0 ? cleaned : undefined;
|
|
154
153
|
}
|
|
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
|
-
}
|
|
162
154
|
/**
|
|
163
155
|
* Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
|
|
164
156
|
* all behavior lives here so tests exercise it directly.
|
|
@@ -185,13 +177,13 @@ async function handleRecallIndex(deps, body) {
|
|
|
185
177
|
const minSimilarity = clampNumber(deps.options?.minSimilarity, DEFAULT_MIN_SIMILARITY, 0, 1);
|
|
186
178
|
const turn = deps.registry.beginTurn(sessionId);
|
|
187
179
|
// Optional client-side scoping (F1 + #203): project + mission_domains (soft
|
|
188
|
-
// affinity)
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
180
|
+
// affinity) ride the body and are pushed into retrieval. project is cwd-
|
|
181
|
+
// derived (CC/OC) or gateway-supplied; mission_domains is Hermes-declared
|
|
182
|
+
// (plugin config). Neither excludes anything — both are zero-boost-neutral
|
|
183
|
+
// score terms in computeScore. (0.16.x: `privacy` is no longer threaded —
|
|
184
|
+
// vestigial column, never filtered; a plugin's privacy_filter is a no-op.)
|
|
192
185
|
const filters = {
|
|
193
186
|
project: typeof req.project === "string" && req.project ? req.project : undefined,
|
|
194
|
-
privacy: parsePrivacyParam(req.privacy),
|
|
195
187
|
mission_domains: parseStringListParam(req.mission_domains),
|
|
196
188
|
};
|
|
197
189
|
let results;
|
|
@@ -238,10 +230,11 @@ async function handleRecallIndex(deps, body) {
|
|
|
238
230
|
*
|
|
239
231
|
* - Short/prefix ids resolve via storage.resolveMemoryId (F6) — the 8-char
|
|
240
232
|
* citation ids agents are taught must work here like on /update, /delete.
|
|
241
|
-
* - Optional `privacy` filter (array or CSV): when present and the memory's
|
|
242
|
-
* privacy level is not in the allowed set, respond 404 with the SAME
|
|
243
|
-
* not-found message — a scoped client must not learn the memory exists.
|
|
244
233
|
* - A successful fetch is real use: access_count + 1 (strengthen).
|
|
234
|
+
*
|
|
235
|
+
* 0.16.x: the `privacy` filter gate was removed — the column is vestigial and
|
|
236
|
+
* never filtered. Callers may still send a `privacy` field (backward compat)
|
|
237
|
+
* but it is ignored.
|
|
245
238
|
*/
|
|
246
239
|
function handleMemoryGet(db, query) {
|
|
247
240
|
const id = typeof query.id === "string" ? query.id : "";
|
|
@@ -257,9 +250,6 @@ function handleMemoryGet(db, query) {
|
|
|
257
250
|
const mem = storage.getMemory(db, fullId);
|
|
258
251
|
if (!mem)
|
|
259
252
|
return notFound;
|
|
260
|
-
const privacy = parsePrivacyParam(query.privacy);
|
|
261
|
-
if (privacy && !privacy.includes(mem.privacy))
|
|
262
|
-
return notFound;
|
|
263
253
|
storage.strengthenMemory(db, fullId, new Date().toISOString());
|
|
264
254
|
// `citation` is server-rendered so every plugin surfaces the same built-in
|
|
265
255
|
// provenance norm (owner directive 27.07) — see #193.
|
package/dist/retrieval.d.ts
CHANGED
|
@@ -178,10 +178,12 @@ export interface EmbedFn {
|
|
|
178
178
|
*
|
|
179
179
|
* #203 retrieval scoping: `project` and `missionDomains` are SOFT affinity
|
|
180
180
|
* terms in computeScore (zero-boost neutral, never a penalty), NOT filters.
|
|
181
|
-
* `privacy`
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
181
|
+
* `privacy` is NOT a filter (0.16.x: the column is fully vestigial — stored,
|
|
182
|
+
* never filtered; the distiller no longer sets it and retrieval ignores it
|
|
183
|
+
* entirely). `sourceAgent` remains a hard filter (kept for completeness; no
|
|
184
|
+
* production caller currently passes it). When neither project nor
|
|
185
|
+
* missionDomains is sent, scoring is byte-identical to pre-#203 — the
|
|
186
|
+
* kill-switch / no-op guarantee.
|
|
185
187
|
*/
|
|
186
188
|
export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query: string, options?: {
|
|
187
189
|
limit?: number;
|
|
@@ -189,7 +191,6 @@ export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query:
|
|
|
189
191
|
* Formerly a hard WHERE filter (#192); softening removes cross-scope
|
|
190
192
|
* starvation without excluding anything. */
|
|
191
193
|
project?: string | null;
|
|
192
|
-
privacy?: string[];
|
|
193
194
|
sourceAgent?: string;
|
|
194
195
|
/** #203: Hermes mission domains (declared in plugin config). Soft domain
|
|
195
196
|
* affinity in computeScore via max overlapping memory_tags.weight. */
|
|
@@ -206,11 +207,10 @@ export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query:
|
|
|
206
207
|
queryEmbedding?: Float32Array;
|
|
207
208
|
}): Promise<MemorySearchResult[]>;
|
|
208
209
|
/**
|
|
209
|
-
* Get recent context, optionally filtered by project
|
|
210
|
+
* Get recent context, optionally filtered by project.
|
|
210
211
|
*/
|
|
211
212
|
export declare function searchRecent(db: Database.Database, options?: {
|
|
212
213
|
project?: string | null;
|
|
213
214
|
limit?: number;
|
|
214
|
-
privacy?: string[];
|
|
215
215
|
}): MemorySearchResult[];
|
|
216
216
|
export {};
|
package/dist/retrieval.js
CHANGED
|
@@ -510,15 +510,16 @@ function reciprocalRankFusion(rankedLists, k = DEFAULT_RRF_K) {
|
|
|
510
510
|
*
|
|
511
511
|
* #203 retrieval scoping: `project` and `missionDomains` are SOFT affinity
|
|
512
512
|
* terms in computeScore (zero-boost neutral, never a penalty), NOT filters.
|
|
513
|
-
* `privacy`
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
513
|
+
* `privacy` is NOT a filter (0.16.x: the column is fully vestigial — stored,
|
|
514
|
+
* never filtered; the distiller no longer sets it and retrieval ignores it
|
|
515
|
+
* entirely). `sourceAgent` remains a hard filter (kept for completeness; no
|
|
516
|
+
* production caller currently passes it). When neither project nor
|
|
517
|
+
* missionDomains is sent, scoring is byte-identical to pre-#203 — the
|
|
518
|
+
* kill-switch / no-op guarantee.
|
|
517
519
|
*/
|
|
518
520
|
async function retrieve(db, embedFn, query, options) {
|
|
519
521
|
const limit = options?.limit ?? recallDefaults.searchLimit;
|
|
520
522
|
const project = options?.project;
|
|
521
|
-
const privacy = options?.privacy;
|
|
522
523
|
const sourceAgent = options?.sourceAgent;
|
|
523
524
|
const missionDomains = options?.missionDomains;
|
|
524
525
|
const now = new Date();
|
|
@@ -534,15 +535,17 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
534
535
|
// over-fetch — the old flat limit*3 intersected a global top-15 with (for the
|
|
535
536
|
// median project) ~1% of the corpus, starving every filtered query.
|
|
536
537
|
// #203: project is NO LONGER a filter (soft affinity now), so it does not
|
|
537
|
-
// trigger over-fetch;
|
|
538
|
-
|
|
538
|
+
// trigger over-fetch; only sourceAgent still does (it remains a hard filter).
|
|
539
|
+
// 0.16.x: privacy is no longer a filter either (column is vestigial).
|
|
540
|
+
const filtered = Boolean(sourceAgent);
|
|
539
541
|
const fetchLimit = filtered ? Math.min(limit * 20, 200) : limit * 3;
|
|
540
542
|
let vecCandidates = storage.vectorSearch(db, queryEmbedding, fetchLimit, []);
|
|
541
543
|
let ftsCandidates = [];
|
|
542
544
|
try {
|
|
543
|
-
//
|
|
544
|
-
// is
|
|
545
|
-
|
|
545
|
+
// sourceAgent is pushed into the FTS SQL (hard filter). project is NOT (it
|
|
546
|
+
// is a soft affinity boost in computeScore as of #203). privacy is NOT
|
|
547
|
+
// (0.16.x: vestigial column, never filtered).
|
|
548
|
+
ftsCandidates = storage.searchFts(db, query, fetchLimit, sourceAgent);
|
|
546
549
|
}
|
|
547
550
|
catch {
|
|
548
551
|
// FTS5 search can fail on special characters; fall back to vector-only
|
|
@@ -550,12 +553,9 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
550
553
|
if (vecCandidates.length === 0 && ftsCandidates.length === 0) {
|
|
551
554
|
return [];
|
|
552
555
|
}
|
|
553
|
-
// Post-filter vector candidates (sqlite-vec can't filter).
|
|
554
|
-
// hard filter (
|
|
555
|
-
//
|
|
556
|
-
if (privacy) {
|
|
557
|
-
vecCandidates = vecCandidates.filter((c) => privacy.includes(c.privacy));
|
|
558
|
-
}
|
|
556
|
+
// Post-filter vector candidates (sqlite-vec can't filter). sourceAgent stays
|
|
557
|
+
// a hard filter (see options doc); project is scored not filtered (#203);
|
|
558
|
+
// privacy is no longer filtered (0.16.x — vestigial).
|
|
559
559
|
if (sourceAgent) {
|
|
560
560
|
vecCandidates = vecCandidates.filter((c) => c.source_agent === sourceAgent);
|
|
561
561
|
}
|
|
@@ -593,9 +593,8 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
593
593
|
if (!mem)
|
|
594
594
|
continue;
|
|
595
595
|
// #203: project check removed — project is a soft affinity in computeScore,
|
|
596
|
-
// not a filter. privacy
|
|
597
|
-
|
|
598
|
-
continue;
|
|
596
|
+
// not a filter. 0.16.x: privacy check removed — the column is vestigial,
|
|
597
|
+
// never filtered. sourceAgent stays a hard filter.
|
|
599
598
|
if (sourceAgent && mem.source_agent !== sourceAgent)
|
|
600
599
|
continue;
|
|
601
600
|
candidateMap.set(gid, { mem, distance: DEFAULT_GRAPH_DISTANCE, source: "graph" });
|
|
@@ -677,12 +676,11 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
677
676
|
return results;
|
|
678
677
|
}
|
|
679
678
|
/**
|
|
680
|
-
* Get recent context, optionally filtered by project
|
|
679
|
+
* Get recent context, optionally filtered by project.
|
|
681
680
|
*/
|
|
682
681
|
function searchRecent(db, options) {
|
|
683
682
|
const limit = options?.limit ?? recallDefaults.recentLimit;
|
|
684
683
|
const project = options?.project;
|
|
685
|
-
const privacy = options?.privacy;
|
|
686
684
|
const now = new Date();
|
|
687
685
|
// #192 breadth: 30 → 180-day default window (config recentWindowDays).
|
|
688
686
|
// "Recent" for a long-lived corpus is a season, not a month; the narrow
|
|
@@ -691,9 +689,7 @@ function searchRecent(db, options) {
|
|
|
691
689
|
if (project) {
|
|
692
690
|
candidates = candidates.filter((c) => c.project === project);
|
|
693
691
|
}
|
|
694
|
-
|
|
695
|
-
candidates = candidates.filter((c) => privacy.includes(c.privacy));
|
|
696
|
-
}
|
|
692
|
+
// 0.16.x: privacy filter removed — the column is vestigial, never filtered.
|
|
697
693
|
if (candidates.length === 0)
|
|
698
694
|
return [];
|
|
699
695
|
const allIds = candidates.map((c) => c.id);
|
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
* embedding of the domain's config description instead.
|
|
12
12
|
* - weight(memory, tag) = cosine(memory embedding, prototype(tag)). Both
|
|
13
13
|
* vectors are L2-normalized, so cosine reduces to a dot product.
|
|
14
|
-
* - PRIMARY (memories.domain) = argmax-weight tag,
|
|
15
|
-
*
|
|
16
|
-
* the owner's Work firewall). Fully mechanical, no LLM.
|
|
14
|
+
* - PRIMARY (memories.domain) = argmax-weight tag, with LLM tag order
|
|
15
|
+
* breaking exact-weight ties. Fully mechanical, no LLM.
|
|
17
16
|
*
|
|
18
17
|
* The LLM decides ONLY the discrete part (which schemas apply — see
|
|
19
18
|
* domain-classify.ts); ALL gradation is derived from embeddings here.
|
|
@@ -65,24 +64,20 @@ export interface WeightedTag {
|
|
|
65
64
|
tag: string;
|
|
66
65
|
weight: number | null;
|
|
67
66
|
}
|
|
68
|
-
/** The configured compartment domain names (DomainDef.compartment === true). */
|
|
69
|
-
export declare function compartmentSet(domains: DomainDef[]): Set<string>;
|
|
70
67
|
/**
|
|
71
68
|
* Derive the PRIMARY tag (memories.domain) from a weighted tag set.
|
|
72
69
|
*
|
|
73
70
|
* Rules (deterministic, no LLM):
|
|
74
|
-
* 1.
|
|
75
|
-
* (unusual) case of several arises.
|
|
76
|
-
* 2. Else the argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
71
|
+
* 1. The argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
77
72
|
* order: ties (and all-null weights) resolve to the EARLIEST array
|
|
78
73
|
* position — strict `>` comparison keeps the first maximum.
|
|
79
|
-
*
|
|
74
|
+
* 2. A null weight loses to any numeric weight (treated as -Infinity).
|
|
80
75
|
*
|
|
81
76
|
* Throws on an empty tag set — callers guarantee >= 1 tag (an empty tag set
|
|
82
77
|
* from the classifier is a NO-FIT and must be routed through nofit.ts, never
|
|
83
78
|
* here); an empty set reaching this function is a programming error.
|
|
84
79
|
*/
|
|
85
|
-
export declare function derivePrimary(tags: WeightedTag[]
|
|
80
|
+
export declare function derivePrimary(tags: WeightedTag[]): string;
|
|
86
81
|
export interface PrototypeStat {
|
|
87
82
|
domain: string;
|
|
88
83
|
memberCount: number;
|
|
@@ -151,9 +146,9 @@ export declare function recomputeAllTagWeights(db: Database.Database, prototypes
|
|
|
151
146
|
};
|
|
152
147
|
/**
|
|
153
148
|
* Re-derive the PRIMARY (memories.domain) of every tagged memory from its
|
|
154
|
-
* current tag weights:
|
|
155
|
-
*
|
|
156
|
-
*
|
|
149
|
+
* current tag weights: argmax weight, LLM order (memory_tags insertion order =
|
|
150
|
+
* rowid, written most-relevant-first by storage.setMemoryTags) breaking
|
|
151
|
+
* exact-weight ties.
|
|
157
152
|
*
|
|
158
153
|
* Memories with NO memory_tags rows are untouched (e.g. infra-skipped rows
|
|
159
154
|
* awaiting classification — issue #150 discipline).
|
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* embedding of the domain's config description instead.
|
|
13
13
|
* - weight(memory, tag) = cosine(memory embedding, prototype(tag)). Both
|
|
14
14
|
* vectors are L2-normalized, so cosine reduces to a dot product.
|
|
15
|
-
* - PRIMARY (memories.domain) = argmax-weight tag,
|
|
16
|
-
*
|
|
17
|
-
* the owner's Work firewall). Fully mechanical, no LLM.
|
|
15
|
+
* - PRIMARY (memories.domain) = argmax-weight tag, with LLM tag order
|
|
16
|
+
* breaking exact-weight ties. Fully mechanical, no LLM.
|
|
18
17
|
*
|
|
19
18
|
* The LLM decides ONLY the discrete part (which schemas apply — see
|
|
20
19
|
* domain-classify.ts); ALL gradation is derived from embeddings here.
|
|
@@ -30,7 +29,6 @@ exports.blobToVec = blobToVec;
|
|
|
30
29
|
exports.l2Normalize = l2Normalize;
|
|
31
30
|
exports.weightedAdd = weightedAdd;
|
|
32
31
|
exports.tagWeight = tagWeight;
|
|
33
|
-
exports.compartmentSet = compartmentSet;
|
|
34
32
|
exports.derivePrimary = derivePrimary;
|
|
35
33
|
exports.loadDomainPrototypes = loadDomainPrototypes;
|
|
36
34
|
exports.computeDomainPrototypes = computeDomainPrototypes;
|
|
@@ -107,33 +105,23 @@ function tagWeight(memoryEmbedding, prototype) {
|
|
|
107
105
|
dot += memoryEmbedding[i] * prototype[i];
|
|
108
106
|
return dot;
|
|
109
107
|
}
|
|
110
|
-
/** The configured compartment domain names (DomainDef.compartment === true). */
|
|
111
|
-
function compartmentSet(domains) {
|
|
112
|
-
return new Set(domains.filter((d) => d.compartment === true).map((d) => d.name));
|
|
113
|
-
}
|
|
114
108
|
/**
|
|
115
109
|
* Derive the PRIMARY tag (memories.domain) from a weighted tag set.
|
|
116
110
|
*
|
|
117
111
|
* Rules (deterministic, no LLM):
|
|
118
|
-
* 1.
|
|
119
|
-
* (unusual) case of several arises.
|
|
120
|
-
* 2. Else the argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
112
|
+
* 1. The argmax-weight tag. `tags` MUST be in LLM most-relevant-first
|
|
121
113
|
* order: ties (and all-null weights) resolve to the EARLIEST array
|
|
122
114
|
* position — strict `>` comparison keeps the first maximum.
|
|
123
|
-
*
|
|
115
|
+
* 2. A null weight loses to any numeric weight (treated as -Infinity).
|
|
124
116
|
*
|
|
125
117
|
* Throws on an empty tag set — callers guarantee >= 1 tag (an empty tag set
|
|
126
118
|
* from the classifier is a NO-FIT and must be routed through nofit.ts, never
|
|
127
119
|
* here); an empty set reaching this function is a programming error.
|
|
128
120
|
*/
|
|
129
|
-
function derivePrimary(tags
|
|
121
|
+
function derivePrimary(tags) {
|
|
130
122
|
if (tags.length === 0) {
|
|
131
123
|
throw new Error("derivePrimary: empty tag set (callers must pass >= 1 tag)");
|
|
132
124
|
}
|
|
133
|
-
for (const t of tags) {
|
|
134
|
-
if (compartments.has(t.tag))
|
|
135
|
-
return t.tag;
|
|
136
|
-
}
|
|
137
125
|
let best = tags[0];
|
|
138
126
|
let bestWeight = best.weight ?? Number.NEGATIVE_INFINITY;
|
|
139
127
|
for (let i = 1; i < tags.length; i++) {
|
|
@@ -313,15 +301,18 @@ function recomputeAllTagWeights(db, prototypes) {
|
|
|
313
301
|
}
|
|
314
302
|
/**
|
|
315
303
|
* Re-derive the PRIMARY (memories.domain) of every tagged memory from its
|
|
316
|
-
* current tag weights:
|
|
317
|
-
*
|
|
318
|
-
*
|
|
304
|
+
* current tag weights: argmax weight, LLM order (memory_tags insertion order =
|
|
305
|
+
* rowid, written most-relevant-first by storage.setMemoryTags) breaking
|
|
306
|
+
* exact-weight ties.
|
|
319
307
|
*
|
|
320
308
|
* Memories with NO memory_tags rows are untouched (e.g. infra-skipped rows
|
|
321
309
|
* awaiting classification — issue #150 discipline).
|
|
322
310
|
*/
|
|
323
311
|
function refreshPrimaries(db, domains) {
|
|
324
|
-
|
|
312
|
+
// `domains` is accepted for API symmetry with the other reconsolidation
|
|
313
|
+
// passes (which need prototypes/weights); the primary is now pure argmax
|
|
314
|
+
// and does not depend on the domain set.
|
|
315
|
+
void domains;
|
|
325
316
|
const rows = db
|
|
326
317
|
.prepare(`SELECT mt.memory_id, mt.tag, mt.weight, m.domain
|
|
327
318
|
FROM memory_tags mt JOIN memories m ON m.id = mt.memory_id
|
|
@@ -341,7 +332,7 @@ function refreshPrimaries(db, domains) {
|
|
|
341
332
|
let updated = 0;
|
|
342
333
|
const tx = db.transaction(() => {
|
|
343
334
|
for (const [memoryId, entry] of byMemory) {
|
|
344
|
-
const primary = derivePrimary(entry.tags
|
|
335
|
+
const primary = derivePrimary(entry.tags);
|
|
345
336
|
if (primary !== entry.domain) {
|
|
346
337
|
update.run(primary, memoryId);
|
|
347
338
|
updated++;
|
package/dist/seed-lesson.js
CHANGED
package/dist/storage.d.ts
CHANGED
|
@@ -54,11 +54,6 @@ export interface SetMemoryTagsOptions {
|
|
|
54
54
|
* (repaired by the next nightly recompute).
|
|
55
55
|
*/
|
|
56
56
|
weights?: Record<string, number | null>;
|
|
57
|
-
/**
|
|
58
|
-
* Compartment domain names (DomainDef.compartment === true): a tagged
|
|
59
|
-
* compartment domain becomes the primary regardless of weights.
|
|
60
|
-
*/
|
|
61
|
-
compartments?: Set<string>;
|
|
62
57
|
}
|
|
63
58
|
/**
|
|
64
59
|
* Set a memory's classification tags (graded schema model).
|
|
@@ -69,9 +64,8 @@ export interface SetMemoryTagsOptions {
|
|
|
69
64
|
* row stores its association weight (NULL when not yet computed).
|
|
70
65
|
*
|
|
71
66
|
* The PRIMARY (memories.domain) is DERIVED here — never passed in by the LLM:
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* weights never diverge.
|
|
67
|
+
* argmax weight, else first tag (all-null weights). The whole update is one
|
|
68
|
+
* transaction so domain, tag set, and weights never diverge.
|
|
75
69
|
*
|
|
76
70
|
* @returns the derived primary written to memories.domain
|
|
77
71
|
*/
|
|
@@ -149,9 +143,10 @@ export declare function getBm25Weights(): Bm25Weights;
|
|
|
149
143
|
*
|
|
150
144
|
* `project` is NOT a filter here (#203): the hard project WHERE from #192 was
|
|
151
145
|
* removed — project is now a soft affinity boost in retrieval.computeScore AND
|
|
152
|
-
* a weighted field in BM25F (#205). `privacy`
|
|
153
|
-
*
|
|
154
|
-
*
|
|
146
|
+
* a weighted field in BM25F (#205). `privacy` is NOT a filter (0.16.x: the
|
|
147
|
+
* column is fully vestigial — stored, never filtered; the privacy IN-clause
|
|
148
|
+
* was removed). `sourceAgent` stays a hard filter (kept for completeness; no
|
|
149
|
+
* production caller of retrieve() currently passes it).
|
|
155
150
|
*
|
|
156
151
|
* #205 sign handling: FTS5's `bm25(table, w0, w1, …)` returns a NEGATIVE score
|
|
157
152
|
* where MORE-negative = better match (it is 1 − the normalized BM25 score,
|
|
@@ -162,7 +157,7 @@ export declare function getBm25Weights(): Bm25Weights;
|
|
|
162
157
|
* positionally as parameters (NOT string-interpolated) so query-planner
|
|
163
158
|
* caching is unaffected and the config path is the only editor.
|
|
164
159
|
*/
|
|
165
|
-
export declare function searchFts(db: Database.Database, query: string, limit?: number,
|
|
160
|
+
export declare function searchFts(db: Database.Database, query: string, limit?: number, sourceAgent?: string): Array<Memory & {
|
|
166
161
|
rank: number;
|
|
167
162
|
}>;
|
|
168
163
|
/**
|
|
@@ -184,6 +179,8 @@ export declare function insertMemoriesBatch(db: Database.Database, memories: Arr
|
|
|
184
179
|
content: string;
|
|
185
180
|
embedding: Float32Array;
|
|
186
181
|
sourceAgent?: string;
|
|
182
|
+
sourceAgentId?: string | null;
|
|
183
|
+
sourceDomain?: string | null;
|
|
187
184
|
sourceSession?: string | null;
|
|
188
185
|
project?: string | null;
|
|
189
186
|
privacy?: string;
|