@gamaze/hicortex 0.15.3 → 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/README.md +2 -1
- package/dist/consolidate.d.ts +4 -3
- package/dist/consolidate.js +33 -12
- package/dist/db.js +83 -3
- package/dist/eval/recall-sweep.d.ts +23 -0
- package/dist/eval/recall-sweep.js +288 -2
- package/dist/index.js +13 -4
- package/dist/init.d.ts +1 -1
- package/dist/init.js +53 -100
- package/dist/llm.d.ts +3 -1
- package/dist/llm.js +18 -4
- package/dist/mcp-server.js +23 -19
- package/dist/memory-instructions.js +1 -1
- package/dist/recall-hook-cli.d.ts +1 -1
- package/dist/recall-hook-cli.js +7 -2
- package/dist/recall-index.d.ts +33 -2
- package/dist/recall-index.js +61 -14
- package/dist/retrieval.d.ts +52 -1
- package/dist/retrieval.js +144 -23
- package/dist/storage.d.ts +56 -3
- package/dist/storage.js +93 -17
- package/dist/types.d.ts +4 -0
- package/dist/uninstall.js +18 -4
- package/hermes-plugin/hicortex/client.py +8 -4
- package/hermes-plugin/hicortex/config.py +11 -0
- package/hermes-plugin/hicortex/provider.py +11 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/hicortex-activate/SKILL.md +0 -53
- package/skills/hicortex-learn/SKILL.md +0 -40
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ The run is resumable — interrupt it any time and it continues where it stopped
|
|
|
122
122
|
- **hicortex_update** — Fix incorrect memories (re-embeds on content change)
|
|
123
123
|
- **hicortex_delete** — Remove memories with cascade cleanup
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
Explicit learnings: call `hicortex_ingest` directly (capture is otherwise automatic, nightly).
|
|
126
126
|
|
|
127
127
|
## Context Layer
|
|
128
128
|
|
|
@@ -202,6 +202,7 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
202
202
|
| `models` | Optional nested per-stage model overrides (`score`/`distill`/`reflect`/`classify`) — see [Advanced: per-stage models](#advanced-per-stage-models) |
|
|
203
203
|
| `distillFallback` | `"strict"` (default) — abort on remote distill failure, retry next run; `"local"` — fall back to base model (lower quality, 0.9.0 behaviour) |
|
|
204
204
|
| `authToken` | Bearer token for endpoint auth. Generated on first `init` in server mode. Find the active token with `hicortex status` or in `~/.hicortex/config.json`. |
|
|
205
|
+
| `corsAllowedOrigins` | Browser origins allowed to read cross-origin responses, e.g. `["https://ui.example.com"]`. **Empty by default** — the server sends no `Access-Control-Allow-Origin` and never `Allow-Credentials`, so no external web page can read its data. The bundled `/viz` and `/context/ui` pages are same-origin and need no entry. |
|
|
205
206
|
| `licenseKey` | Commercial license key (optional; for display in `hicortex status`) |
|
|
206
207
|
| `domains` | Your memory domain list (`[{name, description}]`). Scaffolded by `init`; edit freely — see [Memory Domains & Tags](#memory-domains--tags) |
|
|
207
208
|
| `weakPrimaryFloor` | Minimum similarity for a no-fit memory to keep a weak domain association (default: 0.45) |
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -162,9 +162,10 @@ export declare function buildSupersessionPrompt(oldContent: string, newContent:
|
|
|
162
162
|
export declare function parseSupersessionReply(reply: string): boolean | null;
|
|
163
163
|
/**
|
|
164
164
|
* Nightly supersession-detection stage. Scans memories/rowid > cursor whose
|
|
165
|
-
* shape
|
|
166
|
-
* same-shape neighbors, and links confirmed
|
|
167
|
-
* discovery + the free idempotency check only —
|
|
165
|
+
* shape is supersedable (decision/correction/fact/state — isSupersedableShape),
|
|
166
|
+
* checks each against its older same-shape neighbors, and links confirmed
|
|
167
|
+
* supersessions. Dry-run performs discovery + the free idempotency check only —
|
|
168
|
+
* no LLM calls, no writes, no
|
|
168
169
|
* cursor persistence (mirrors stageImportance/stageContentDomains's dry-run
|
|
169
170
|
* convention of never spending budget on a preview).
|
|
170
171
|
*
|
package/dist/consolidate.js
CHANGED
|
@@ -857,11 +857,21 @@ const SUPERSESSION_NEIGHBOR_POOL = 15;
|
|
|
857
857
|
const SUPERSESSION_NEIGHBOR_TOP_K = 5;
|
|
858
858
|
/** Candidate rows read per SQL page (call budget stops the loop well before this in practice). */
|
|
859
859
|
const SUPERSESSION_BATCH_SIZE = 500;
|
|
860
|
-
/**
|
|
861
|
-
|
|
860
|
+
/**
|
|
861
|
+
* A memory whose content/type marks it as a SUPERSEDABLE claim — one a newer
|
|
862
|
+
* memory about the same subject can replace. Decisions and corrections were the
|
|
863
|
+
* original scope; plain facts and project-state updates were added because an
|
|
864
|
+
* updated fact ("distillModel is X" → later "is Y") otherwise never gets a
|
|
865
|
+
* superseded_by link and both versions compete in recall forever. Ordinary
|
|
866
|
+
* episodic chatter and problem/solution history stay excluded: they record
|
|
867
|
+
* events, not mutable state, so there is nothing to supersede.
|
|
868
|
+
*/
|
|
869
|
+
function isSupersedableShape(mem) {
|
|
862
870
|
return (mem.memory_type === "decision" ||
|
|
863
871
|
mem.content.includes("[Decisions Made]") ||
|
|
864
|
-
mem.content.includes("[Corrections & Rejections]")
|
|
872
|
+
mem.content.includes("[Corrections & Rejections]") ||
|
|
873
|
+
mem.content.includes("[Facts Learned]") ||
|
|
874
|
+
mem.content.includes("[Project State Changes]"));
|
|
865
875
|
}
|
|
866
876
|
/** True when a `superseded_by` link already exists between the pair, either direction. */
|
|
867
877
|
function alreadySupersedeLinked(db, oldId, newId) {
|
|
@@ -881,9 +891,12 @@ function buildSupersessionPrompt(oldContent, newContent) {
|
|
|
881
891
|
return (`You are checking whether a NEWER memory supersedes an OLDER one in an AI agent's long-term memory.\n\n` +
|
|
882
892
|
`OLDER MEMORY:\n${trunc(oldContent)}\n\n` +
|
|
883
893
|
`NEWER MEMORY:\n${trunc(newContent)}\n\n` +
|
|
884
|
-
`Does the NEWER memory reverse, replace, or invalidate the OLDER one — e.g. a later decision
|
|
885
|
-
`earlier one,
|
|
886
|
-
`
|
|
894
|
+
`Does the NEWER memory reverse, replace, update, or invalidate the OLDER one — e.g. a later decision ` +
|
|
895
|
+
`overturns an earlier one, a correction retracts a prior claim, or a later fact updates the SAME subject's ` +
|
|
896
|
+
`value/status that has since changed (e.g. "model is X" → "model is Y")? Reply true ONLY for a genuine ` +
|
|
897
|
+
`replacement of the same fact/decision. Two memories that are merely related, or that can both still be ` +
|
|
898
|
+
`true — even about the same project or entity (different facts, an addition, an elaboration) — are NOT a ` +
|
|
899
|
+
`supersession.\n` +
|
|
887
900
|
`Reply with ONLY a JSON object, no prose: {"superseded": true} or {"superseded": false}.`);
|
|
888
901
|
}
|
|
889
902
|
/**
|
|
@@ -932,16 +945,17 @@ async function findOlderNeighbors(db, candidate, embedFn, minSimilarity) {
|
|
|
932
945
|
return storage
|
|
933
946
|
.vectorSearch(db, embedding, SUPERSESSION_NEIGHBOR_POOL, [candidate.id])
|
|
934
947
|
.filter((n) => n.created_at < candidate.created_at &&
|
|
935
|
-
|
|
948
|
+
isSupersedableShape(n) &&
|
|
936
949
|
(0, retrieval_js_1.l2ToCosine)(n.distance) >= minSimilarity)
|
|
937
950
|
.sort((a, b) => (0, retrieval_js_1.l2ToCosine)(b.distance) - (0, retrieval_js_1.l2ToCosine)(a.distance))
|
|
938
951
|
.slice(0, SUPERSESSION_NEIGHBOR_TOP_K);
|
|
939
952
|
}
|
|
940
953
|
/**
|
|
941
954
|
* Nightly supersession-detection stage. Scans memories/rowid > cursor whose
|
|
942
|
-
* shape
|
|
943
|
-
* same-shape neighbors, and links confirmed
|
|
944
|
-
* discovery + the free idempotency check only —
|
|
955
|
+
* shape is supersedable (decision/correction/fact/state — isSupersedableShape),
|
|
956
|
+
* checks each against its older same-shape neighbors, and links confirmed
|
|
957
|
+
* supersessions. Dry-run performs discovery + the free idempotency check only —
|
|
958
|
+
* no LLM calls, no writes, no
|
|
945
959
|
* cursor persistence (mirrors stageImportance/stageContentDomains's dry-run
|
|
946
960
|
* convention of never spending budget on a preview).
|
|
947
961
|
*
|
|
@@ -965,9 +979,16 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
965
979
|
const maxCalls = validNumber(options.maxCalls, exports.DEFAULT_SUPERSESSION_MAX_CALLS, (n) => n >= 0);
|
|
966
980
|
const startCursor = (0, state_js_1.loadState)(stateDir).supersessionCursor ?? 0;
|
|
967
981
|
const rows = db
|
|
968
|
-
.prepare(
|
|
982
|
+
.prepare(
|
|
983
|
+
// Candidate shape must mirror isSupersedableShape() exactly — keep the two
|
|
984
|
+
// in lockstep (an inline SQL copy, so drift here silently narrows scope).
|
|
985
|
+
`SELECT rowid AS __rowid, * FROM memories
|
|
969
986
|
WHERE rowid > ?
|
|
970
|
-
AND (memory_type = 'decision'
|
|
987
|
+
AND (memory_type = 'decision'
|
|
988
|
+
OR content LIKE '%[Decisions Made]%'
|
|
989
|
+
OR content LIKE '%[Corrections & Rejections]%'
|
|
990
|
+
OR content LIKE '%[Facts Learned]%'
|
|
991
|
+
OR content LIKE '%[Project State Changes]%')
|
|
971
992
|
ORDER BY rowid ASC LIMIT ?`)
|
|
972
993
|
.all(startCursor, SUPERSESSION_BATCH_SIZE);
|
|
973
994
|
let scanned = 0;
|
package/dist/db.js
CHANGED
|
@@ -126,20 +126,33 @@ CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
|
|
|
126
126
|
CREATE INDEX IF NOT EXISTS idx_links_source ON memory_links(source_id);
|
|
127
127
|
CREATE INDEX IF NOT EXISTS idx_links_target ON memory_links(target_id);
|
|
128
128
|
`;
|
|
129
|
+
// Fielded FTS5 (#205, migration v10 "fts_fielded"): three columns so bm25()
|
|
130
|
+
// can weight matches per field (body / project / domain). Column order matters
|
|
131
|
+
// — the weights passed to `bm25(memories_fts, w_body, w_project, w_domain)` in
|
|
132
|
+
// storage.searchFts are positional on this declaration. `domain` reads the
|
|
133
|
+
// derived PRIMARY `memories.domain` (the argmax-weight tag from the classifier,
|
|
134
|
+
// set nightly), NOT a memory_tags join — that was judged too fiddly for this
|
|
135
|
+
// phase (a multi-table FTS trigger is fragile and rebuilds on every tag edit).
|
|
136
|
+
// `content_rowid='rowid'` is preserved for lockstep with the legacy schema.
|
|
129
137
|
const FTS_SCHEMA = `
|
|
130
138
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
131
139
|
content,
|
|
140
|
+
project,
|
|
141
|
+
domain,
|
|
132
142
|
content_rowid='rowid'
|
|
133
143
|
);
|
|
134
144
|
|
|
135
145
|
CREATE TRIGGER IF NOT EXISTS memories_fts_insert AFTER INSERT ON memories
|
|
136
146
|
BEGIN
|
|
137
|
-
INSERT INTO memories_fts (rowid, content
|
|
147
|
+
INSERT INTO memories_fts (rowid, content, project, domain)
|
|
148
|
+
VALUES (NEW.rowid, NEW.content, NEW.project, NEW.domain);
|
|
138
149
|
END;
|
|
139
150
|
|
|
140
|
-
CREATE TRIGGER IF NOT EXISTS memories_fts_update AFTER UPDATE OF content ON memories
|
|
151
|
+
CREATE TRIGGER IF NOT EXISTS memories_fts_update AFTER UPDATE OF content, project, domain ON memories
|
|
141
152
|
BEGIN
|
|
142
|
-
UPDATE memories_fts
|
|
153
|
+
UPDATE memories_fts
|
|
154
|
+
SET content = NEW.content, project = NEW.project, domain = NEW.domain
|
|
155
|
+
WHERE rowid = NEW.rowid;
|
|
143
156
|
END;
|
|
144
157
|
|
|
145
158
|
CREATE TRIGGER IF NOT EXISTS memories_fts_delete AFTER DELETE ON memories
|
|
@@ -361,6 +374,73 @@ const MIGRATIONS = [
|
|
|
361
374
|
db.exec("CREATE INDEX IF NOT EXISTS idx_dedup_log_source_session ON dedup_log(source_session)");
|
|
362
375
|
},
|
|
363
376
|
},
|
|
377
|
+
{
|
|
378
|
+
version: 10,
|
|
379
|
+
name: "fts_fielded",
|
|
380
|
+
up: (db) => {
|
|
381
|
+
// #205 fielded BM25F. FTS5 cannot be ALTERed (see the virtual-table rule
|
|
382
|
+
// above), so the single-column `memories_fts(content)` from pre-v10 must
|
|
383
|
+
// be dropped + recreated multi-column (`content, project, domain`) and
|
|
384
|
+
// rebuilt from the canonical `memories` rows. The runner wraps up() in a
|
|
385
|
+
// single transaction (db.ts:216) — that transaction IS the crash window:
|
|
386
|
+
// a power loss mid-migration rolls back, leaving the OLD single-column
|
|
387
|
+
// FTS intact (recall falls back to vector-only until the next open;
|
|
388
|
+
// never silently empty). The nightly capture lock does NOT cover
|
|
389
|
+
// initDb migrations, so the tx guard is the only safety net.
|
|
390
|
+
//
|
|
391
|
+
// Idempotent by construction: DROP IF EXISTS + CREATE + rebuild. On a
|
|
392
|
+
// fresh DB (where FTS_SCHEMA already created the multi-column form) this
|
|
393
|
+
// is a 0-row rebuild — wasted but harmless. On a legacy single-column DB
|
|
394
|
+
// it converts in place. Re-running against an already-migrated DB is a
|
|
395
|
+
// no-op shape + a refill of the same rows.
|
|
396
|
+
//
|
|
397
|
+
// Triggers are dropped + recreated to pick up the new column list (the
|
|
398
|
+
// pre-v10 update trigger fired only on `UPDATE OF content`; the new one
|
|
399
|
+
// also fires on project/domain updates so a tag reclassification lands
|
|
400
|
+
// in FTS without a content edit).
|
|
401
|
+
db.exec("DROP TRIGGER IF EXISTS memories_fts_insert");
|
|
402
|
+
db.exec("DROP TRIGGER IF EXISTS memories_fts_update");
|
|
403
|
+
db.exec("DROP TRIGGER IF EXISTS memories_fts_delete");
|
|
404
|
+
db.exec("DROP TABLE IF EXISTS memories_fts");
|
|
405
|
+
db.exec(`
|
|
406
|
+
CREATE VIRTUAL TABLE memories_fts USING fts5(
|
|
407
|
+
content,
|
|
408
|
+
project,
|
|
409
|
+
domain,
|
|
410
|
+
content_rowid='rowid'
|
|
411
|
+
)
|
|
412
|
+
`);
|
|
413
|
+
// COALESCE on project/domain because FTS5 stores NULL as no-tokens,
|
|
414
|
+
// which is what we want for unscoped memories (NULL domain = not yet
|
|
415
|
+
// classified; NULL project = no cwd-derived label). content is NOT NULL
|
|
416
|
+
// by the insert contract.
|
|
417
|
+
db.exec(`
|
|
418
|
+
INSERT INTO memories_fts (rowid, content, project, domain)
|
|
419
|
+
SELECT rowid, content, COALESCE(project, ''), COALESCE(domain, '') FROM memories
|
|
420
|
+
`);
|
|
421
|
+
db.exec(`
|
|
422
|
+
CREATE TRIGGER memories_fts_insert AFTER INSERT ON memories
|
|
423
|
+
BEGIN
|
|
424
|
+
INSERT INTO memories_fts (rowid, content, project, domain)
|
|
425
|
+
VALUES (NEW.rowid, NEW.content, NEW.project, NEW.domain);
|
|
426
|
+
END
|
|
427
|
+
`);
|
|
428
|
+
db.exec(`
|
|
429
|
+
CREATE TRIGGER memories_fts_update AFTER UPDATE OF content, project, domain ON memories
|
|
430
|
+
BEGIN
|
|
431
|
+
UPDATE memories_fts
|
|
432
|
+
SET content = NEW.content, project = NEW.project, domain = NEW.domain
|
|
433
|
+
WHERE rowid = NEW.rowid;
|
|
434
|
+
END
|
|
435
|
+
`);
|
|
436
|
+
db.exec(`
|
|
437
|
+
CREATE TRIGGER memories_fts_delete AFTER DELETE ON memories
|
|
438
|
+
BEGIN
|
|
439
|
+
DELETE FROM memories_fts WHERE rowid = OLD.rowid;
|
|
440
|
+
END
|
|
441
|
+
`);
|
|
442
|
+
},
|
|
443
|
+
},
|
|
364
444
|
];
|
|
365
445
|
/**
|
|
366
446
|
* Run all pending migrations against the database.
|
|
@@ -54,6 +54,29 @@
|
|
|
54
54
|
* badly post-shift. Report written to data/eval-recall-sweep/report.md and
|
|
55
55
|
* printed to stdout.
|
|
56
56
|
*
|
|
57
|
+
* ## Section 4 — SCOPE dimension (#203 soft project affinity)
|
|
58
|
+
*
|
|
59
|
+
* A THIRD family on a SEPARATE project-labeled corpus (the blend-sweep corpus
|
|
60
|
+
* above has no project labels). Goal: prove #203's soft project affinity drops
|
|
61
|
+
* cross-scope noise (the boat/battery case — a "hardware" query surfacing
|
|
62
|
+
* "marine" memories on a shared token like "battery") below the cap WITHOUT a
|
|
63
|
+
* hard filter, and WITHOUT losing same-scope recall.
|
|
64
|
+
*
|
|
65
|
+
* Corpus: 6 hardware + 6 marine memories. 3 marine memories are
|
|
66
|
+
* CONTAMINATION SEEDS (share a token with the hardware queries); 3 are
|
|
67
|
+
* marine-only fillers (control).
|
|
68
|
+
*
|
|
69
|
+
* Each hardware query runs TWICE on the same static DB:
|
|
70
|
+
* scope OFF — no `project` sent (byte-identical to pre-#203).
|
|
71
|
+
* scope ON — `project: "hardware"` (computeScore adds +projectAffinity 0.15
|
|
72
|
+
* to hardware memories; marine gets 0; no hard filter).
|
|
73
|
+
*
|
|
74
|
+
* Metrics: contamination@5 (marine in top-5 / 5 — LOWER is better),
|
|
75
|
+
* recall@5 (gold hardware memory surfaced), sameScope@5 (hardware in top-5).
|
|
76
|
+
* Scope is orthogonal to the blend, so the blend weight is held at 0 here —
|
|
77
|
+
* `project` is the ONLY variable. Same invariants (noStrengthen, real
|
|
78
|
+
* embedder, uniform metadata, neverCalledEmbed self-check).
|
|
79
|
+
*
|
|
57
80
|
* Run: npm run eval:recall-sweep (== node dist/eval/recall-sweep.js)
|
|
58
81
|
*/
|
|
59
82
|
export {};
|
|
@@ -55,6 +55,29 @@
|
|
|
55
55
|
* badly post-shift. Report written to data/eval-recall-sweep/report.md and
|
|
56
56
|
* printed to stdout.
|
|
57
57
|
*
|
|
58
|
+
* ## Section 4 — SCOPE dimension (#203 soft project affinity)
|
|
59
|
+
*
|
|
60
|
+
* A THIRD family on a SEPARATE project-labeled corpus (the blend-sweep corpus
|
|
61
|
+
* above has no project labels). Goal: prove #203's soft project affinity drops
|
|
62
|
+
* cross-scope noise (the boat/battery case — a "hardware" query surfacing
|
|
63
|
+
* "marine" memories on a shared token like "battery") below the cap WITHOUT a
|
|
64
|
+
* hard filter, and WITHOUT losing same-scope recall.
|
|
65
|
+
*
|
|
66
|
+
* Corpus: 6 hardware + 6 marine memories. 3 marine memories are
|
|
67
|
+
* CONTAMINATION SEEDS (share a token with the hardware queries); 3 are
|
|
68
|
+
* marine-only fillers (control).
|
|
69
|
+
*
|
|
70
|
+
* Each hardware query runs TWICE on the same static DB:
|
|
71
|
+
* scope OFF — no `project` sent (byte-identical to pre-#203).
|
|
72
|
+
* scope ON — `project: "hardware"` (computeScore adds +projectAffinity 0.15
|
|
73
|
+
* to hardware memories; marine gets 0; no hard filter).
|
|
74
|
+
*
|
|
75
|
+
* Metrics: contamination@5 (marine in top-5 / 5 — LOWER is better),
|
|
76
|
+
* recall@5 (gold hardware memory surfaced), sameScope@5 (hardware in top-5).
|
|
77
|
+
* Scope is orthogonal to the blend, so the blend weight is held at 0 here —
|
|
78
|
+
* `project` is the ONLY variable. Same invariants (noStrengthen, real
|
|
79
|
+
* embedder, uniform metadata, neverCalledEmbed self-check).
|
|
80
|
+
*
|
|
58
81
|
* Run: npm run eval:recall-sweep (== node dist/eval/recall-sweep.js)
|
|
59
82
|
*/
|
|
60
83
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -301,6 +324,81 @@ const SHIFT_PAIRS = [
|
|
|
301
324
|
["deploy", "testing"],
|
|
302
325
|
];
|
|
303
326
|
const WEIGHTS = [0.0, 0.2, 0.4, 0.5, 0.6, 0.8];
|
|
327
|
+
const SCOPE_CORPUS = [
|
|
328
|
+
// --- hardware (6): the same-scope / gold memories ---
|
|
329
|
+
{
|
|
330
|
+
project: "hardware",
|
|
331
|
+
text: "Lithium-ion battery pack drains 3% per day on standby. Traced to a leaky protection-circuit MOSFET that draws 50mA while the device is powered off.",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
project: "hardware",
|
|
335
|
+
text: "5V power supply brown-out: the rail drooped to 4.2V under peak load. The bulk capacitor was undersized; added a second decoupling stage.",
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
project: "hardware",
|
|
339
|
+
text: "Charge controller IC never enters constant-voltage mode — the battery overcharges because the CC/CV transition threshold was misconfigured in firmware.",
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
project: "hardware",
|
|
343
|
+
text: "ADC battery voltage reading was 40% off: the resistor divider ratio was wrong, so the fuel gauge reported a full pack at 60%. Recomputed and recalibrated.",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
project: "hardware",
|
|
347
|
+
text: "Battery thermal runaway during charging: the NTC thermistor lead was cold-soldered, disabling temperature compensation. Reflowed the joint.",
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
project: "hardware",
|
|
351
|
+
text: "Current-sense shunt drifted under load: the 10 milli-ohm resistor heated and its temperature coefficient skewed the reading. Switched to a low-TCR manganin part.",
|
|
352
|
+
},
|
|
353
|
+
// --- marine SEEDS (3): share a token (battery / drain / voltage / charge /
|
|
354
|
+
// temperature) with the hardware queries — a naive keyword/vector search
|
|
355
|
+
// surfaces these for a hardware query. ---
|
|
356
|
+
{
|
|
357
|
+
project: "marine",
|
|
358
|
+
seed: true,
|
|
359
|
+
text: "Boat battery drained overnight at anchor: the bilge pump ran continuously because the float switch stuck closed. Replaced the switch.",
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
project: "marine",
|
|
363
|
+
seed: true,
|
|
364
|
+
text: "Marine house-bank voltage drop: the battery isolator had corroded terminals, dropping 0.8V under load. Cleaned and greased the lugs.",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
project: "marine",
|
|
368
|
+
seed: true,
|
|
369
|
+
text: "Trolling-motor battery won't reach full charge: the on-board charger undercharges the deep-cycle bank because the temperature-compensation sense lead is on the wrong terminal.",
|
|
370
|
+
},
|
|
371
|
+
// --- marine FILLERS (3): no token overlap with any hardware query. Control
|
|
372
|
+
// row — these should never surface in either mode, and if they do it is a
|
|
373
|
+
// ranking bug, not a scope issue. ---
|
|
374
|
+
{
|
|
375
|
+
project: "marine",
|
|
376
|
+
text: "Jib furler jammed: the halyard wrapped on the swivel. Fitted a halyard deflector and re-led the furling line.",
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
project: "marine",
|
|
380
|
+
text: "Chartplotter took eight minutes to a GPS fix: the antenna was under the fiberglass hard-top. Relocated it to the T-top rail.",
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
project: "marine",
|
|
384
|
+
text: "Ablative antifouling wore thin below the waterline after 18 months. Hauled the boat, barrier-coated, and repainted.",
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
/**
|
|
388
|
+
* 5 hardware-scoped queries. Q1-Q4 each have a marine contamination seed
|
|
389
|
+
* (varying strength: Q1 near-identical, Q3 antonym-semantic, Q4 token-exact);
|
|
390
|
+
* Q5 is the CONTROL — no marine memory shares its tokens, so contamination
|
|
391
|
+
* should be ~0 in both modes (proves the metric isn't trivially inflated).
|
|
392
|
+
*/
|
|
393
|
+
const SCOPE_QUERIES = [
|
|
394
|
+
{ prompt: "battery drain overnight", goldIndices: [0], expect: "strong (M6 near-identical)" },
|
|
395
|
+
{ prompt: "power supply voltage drop", goldIndices: [1], expect: "moderate (M7 voltage)" },
|
|
396
|
+
{ prompt: "battery overcharging", goldIndices: [2], expect: "weak (M8 antonym)" },
|
|
397
|
+
{ prompt: "battery temperature compensation", goldIndices: [4], expect: "strong (M8 token)" },
|
|
398
|
+
{ prompt: "current shunt drift", goldIndices: [5], expect: "control (none)" },
|
|
399
|
+
];
|
|
400
|
+
const SCOPE_K = 5;
|
|
401
|
+
const SCOPE_PROJECT = "hardware";
|
|
304
402
|
function buildSessions() {
|
|
305
403
|
const focused = TOPICS.map((t) => ({
|
|
306
404
|
kind: "focused",
|
|
@@ -340,6 +438,32 @@ async function buildCorpusDb(dbPath) {
|
|
|
340
438
|
}
|
|
341
439
|
return { db, idToTopic };
|
|
342
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* Build the SCOPE corpus DB (separate from the blend-sweep corpus — the scope
|
|
443
|
+
* corpus carries project labels the sweep corpus does not). Same honesty
|
|
444
|
+
* controls: real embedder, uniform metadata (base_strength=0.5, created_at~now,
|
|
445
|
+
* no links). The ONLY addition vs the sweep corpus is `project`, which is the
|
|
446
|
+
* #203 scope label computeScore reads.
|
|
447
|
+
*
|
|
448
|
+
* Returns `ids` in corpus order so SCOPE_QUERIES goldIndices map to ids.
|
|
449
|
+
*/
|
|
450
|
+
async function buildScopeDb(dbPath) {
|
|
451
|
+
const db = (0, db_js_1.initDb)(dbPath);
|
|
452
|
+
const idToProject = new Map();
|
|
453
|
+
const ids = [];
|
|
454
|
+
for (const mem of SCOPE_CORPUS) {
|
|
455
|
+
const vec = await (0, embedder_js_1.embed)(mem.text);
|
|
456
|
+
const id = storage.insertMemory(db, mem.text, vec, {
|
|
457
|
+
sourceAgent: "eval-scope",
|
|
458
|
+
memoryType: "episode",
|
|
459
|
+
baseStrength: 0.5, // uniform — strength is NOT a discriminator here
|
|
460
|
+
project: mem.project, // #203 scope label
|
|
461
|
+
});
|
|
462
|
+
idToProject.set(id, mem.project);
|
|
463
|
+
ids.push(id);
|
|
464
|
+
}
|
|
465
|
+
return { db, idToProject, ids };
|
|
466
|
+
}
|
|
343
467
|
/** Embed every distinct prompt once; return a cache keyed by prompt text. */
|
|
344
468
|
async function embedAllPrompts(sessions) {
|
|
345
469
|
const cache = new Map();
|
|
@@ -649,6 +773,147 @@ function renderReport(focused, shifts, records, sessions, meta) {
|
|
|
649
773
|
L.push("");
|
|
650
774
|
return L.join("\n");
|
|
651
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* Run the scope sweep: each hardware query is run TWICE on the same static DB —
|
|
778
|
+
* scope OFF (no `project` sent, byte-identical to pre-#203) and scope ON
|
|
779
|
+
* (`project: "hardware"`). The #203 affinity boosts hardware memories; marine
|
|
780
|
+
* memories get nothing. No hard filter anywhere.
|
|
781
|
+
*
|
|
782
|
+
* Scope is ORTHOGONAL to the session-intent blend, so the blend weight is held
|
|
783
|
+
* at 0 (pure prompt embedding) — the ONLY variable is whether `project` is in
|
|
784
|
+
* options. `queryEmbedding` is supplied (pure prompt), so retrieve() must never
|
|
785
|
+
* call the embedFn; the `neverCalledEmbed` self-check enforces that.
|
|
786
|
+
*/
|
|
787
|
+
async function runScopeSweep(db, idToProject, ids) {
|
|
788
|
+
// Precompute query embeddings once — pure prompt, no centroid (scope is the
|
|
789
|
+
// only variable; blend held at 0 to isolate it).
|
|
790
|
+
const queryEmb = new Map();
|
|
791
|
+
for (const q of SCOPE_QUERIES) {
|
|
792
|
+
if (!queryEmb.has(q.prompt))
|
|
793
|
+
queryEmb.set(q.prompt, await (0, embedder_js_1.embed)(q.prompt));
|
|
794
|
+
}
|
|
795
|
+
const neverCalledEmbed = async () => {
|
|
796
|
+
throw new Error("recall-sweep (scope): retrieve() called the embedFn — queryEmbedding was not honored. Eval aborted (results would be invalid).");
|
|
797
|
+
};
|
|
798
|
+
const records = [];
|
|
799
|
+
for (let qi = 0; qi < SCOPE_QUERIES.length; qi++) {
|
|
800
|
+
const q = SCOPE_QUERIES[qi];
|
|
801
|
+
const gold = new Set(q.goldIndices.map((i) => ids[i]));
|
|
802
|
+
const promptVec = queryEmb.get(q.prompt);
|
|
803
|
+
for (const mode of ["off", "on"]) {
|
|
804
|
+
const results = await (0, retrieval_js_1.retrieve)(db, neverCalledEmbed, q.prompt, {
|
|
805
|
+
limit: SCOPE_K,
|
|
806
|
+
queryEmbedding: promptVec,
|
|
807
|
+
noStrengthen: true,
|
|
808
|
+
// OFF: omit project entirely (byte-identical to pre-#203).
|
|
809
|
+
// ON: send project — computeScore adds +projectAffinity (0.15) to
|
|
810
|
+
// every hardware memory; marine memories get 0.
|
|
811
|
+
...(mode === "on" ? { project: SCOPE_PROJECT } : {}),
|
|
812
|
+
});
|
|
813
|
+
const topIds = results.map((r) => r.id);
|
|
814
|
+
const marineCount = topIds.filter((id) => idToProject.get(id) === "marine").length;
|
|
815
|
+
const hardwareCount = topIds.filter((id) => idToProject.get(id) === "hardware").length;
|
|
816
|
+
const goldCount = topIds.filter((id) => gold.has(id)).length;
|
|
817
|
+
records.push({
|
|
818
|
+
queryLabel: `Q${qi + 1}`,
|
|
819
|
+
prompt: q.prompt,
|
|
820
|
+
mode,
|
|
821
|
+
recallAt5: goldCount > 0 ? 1 : 0,
|
|
822
|
+
goldP5: goldCount,
|
|
823
|
+
sameScopeP5: hardwareCount,
|
|
824
|
+
contaminationAt5: marineCount / SCOPE_K,
|
|
825
|
+
topIds,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return records;
|
|
830
|
+
}
|
|
831
|
+
function meanScope(rs, field) {
|
|
832
|
+
if (rs.length === 0)
|
|
833
|
+
return 0;
|
|
834
|
+
return rs.reduce((s, r) => s + r[field], 0) / rs.length;
|
|
835
|
+
}
|
|
836
|
+
function renderScopeSection(records) {
|
|
837
|
+
const L = [];
|
|
838
|
+
L.push("## 4. Scope contamination (#203 soft project affinity)\n");
|
|
839
|
+
L.push(`Separate synthetic corpus (project-labeled): ${SCOPE_CORPUS.filter((m) => m.project === "hardware").length} hardware + ` +
|
|
840
|
+
`${SCOPE_CORPUS.filter((m) => m.project === "marine").length} marine memories. ` +
|
|
841
|
+
`${SCOPE_CORPUS.filter((m) => m.project === "marine" && m.seed).length} marine memories are CONTAMINATION SEEDS ` +
|
|
842
|
+
`(share a token — battery / drain / voltage / charge / temperature — with the hardware queries); ` +
|
|
843
|
+
`${SCOPE_CORPUS.filter((m) => m.project === "marine" && !m.seed).length} are marine-only fillers (no token overlap — control). ` +
|
|
844
|
+
`Each hardware query runs TWICE: scope OFF (no \`project\` sent — byte-identical to pre-#203) and scope ON (\`project: "${SCOPE_PROJECT}"\`). ` +
|
|
845
|
+
`The #203 affinity adds +projectAffinity (0.15) to hardware memories in computeScore; marine memories get 0. No hard filter anywhere.\n`);
|
|
846
|
+
L.push("_Same invariants as the blend sweep: static DB (`noStrengthen: true`), real bge-small-en-v1.5 embedder, " +
|
|
847
|
+
"uniform metadata (base_strength=0.5, created_at~now, no links). Blend weight held at 0 (pure prompt) — " +
|
|
848
|
+
"scope is orthogonal to session-intent keying. The `neverCalledEmbed` self-check still passes._\n");
|
|
849
|
+
// ---- Per-query table ----
|
|
850
|
+
L.push("### Per-query results (scope OFF vs ON)\n");
|
|
851
|
+
L.push("contamination@5 = marine memories in top-5 / 5 (**LOWER is better**). recall@5 = the gold hardware memory surfaced (binary). " +
|
|
852
|
+
"sameScope@5 = hardware memories in top-5 (incl. non-gold). goldP@5 = gold count in top-5.\n");
|
|
853
|
+
L.push("| # | query | expected | mode | contamination@5 | recall@5 | sameScope@5 | goldP@5 |");
|
|
854
|
+
L.push("|---|---|---|---|---|---|---|---|");
|
|
855
|
+
for (let qi = 0; qi < SCOPE_QUERIES.length; qi++) {
|
|
856
|
+
const off = records.find((r) => r.queryLabel === `Q${qi + 1}` && r.mode === "off");
|
|
857
|
+
const on = records.find((r) => r.queryLabel === `Q${qi + 1}` && r.mode === "on");
|
|
858
|
+
const q = SCOPE_QUERIES[qi];
|
|
859
|
+
L.push(`| Q${qi + 1} | \`${q.prompt}\` | ${q.expect} | OFF | ${off.contaminationAt5.toFixed(2)} | ${off.recallAt5} | ${off.sameScopeP5} | ${off.goldP5} |`);
|
|
860
|
+
L.push(`| | | | **ON** | **${on.contaminationAt5.toFixed(2)}** | **${on.recallAt5}** | **${on.sameScopeP5}** | **${on.goldP5}** |`);
|
|
861
|
+
}
|
|
862
|
+
L.push("");
|
|
863
|
+
// ---- Aggregate ----
|
|
864
|
+
const offRecs = records.filter((r) => r.mode === "off");
|
|
865
|
+
const onRecs = records.filter((r) => r.mode === "on");
|
|
866
|
+
const contamOff = meanScope(offRecs, "contaminationAt5");
|
|
867
|
+
const contamOn = meanScope(onRecs, "contaminationAt5");
|
|
868
|
+
const recallOff = meanScope(offRecs, "recallAt5");
|
|
869
|
+
const recallOn = meanScope(onRecs, "recallAt5");
|
|
870
|
+
const sameOff = meanScope(offRecs, "sameScopeP5");
|
|
871
|
+
const sameOn = meanScope(onRecs, "sameScopeP5");
|
|
872
|
+
L.push("### Aggregate (mean over all 5 queries)\n");
|
|
873
|
+
L.push("| mode | contamination@5 | recall@5 (gold) | sameScope@5 (hardware) |");
|
|
874
|
+
L.push("|---|---|---|---|");
|
|
875
|
+
L.push(`| OFF (no project) | ${contamOff.toFixed(3)} | ${recallOff.toFixed(3)} | ${sameOff.toFixed(2)} |`);
|
|
876
|
+
L.push(`| ON (project=${SCOPE_PROJECT}) | ${contamOn.toFixed(3)} | ${recallOn.toFixed(3)} | ${sameOn.toFixed(2)} |`);
|
|
877
|
+
L.push("");
|
|
878
|
+
// ---- Verdict ----
|
|
879
|
+
const contamDrop = contamOff - contamOn;
|
|
880
|
+
const recallRegress = recallOn < recallOff - 1e-9;
|
|
881
|
+
const contamMaterial = contamDrop > 0.01; // any measurable mean drop = material
|
|
882
|
+
const pass = contamMaterial && !recallRegress;
|
|
883
|
+
L.push("### Scope verdict (PASS criterion: contamination drops materially AND same-scope recall does not regress)\n");
|
|
884
|
+
L.push(`- contamination@5: OFF ${contamOff.toFixed(3)} → ON ${contamOn.toFixed(3)} ` +
|
|
885
|
+
`(${contamDrop >= 0 ? "−" : "+"}${Math.abs(contamDrop).toFixed(3)}, ${pct(Math.abs(contamDrop))} absolute)`);
|
|
886
|
+
L.push(`- recall@5 (gold surfaced): OFF ${recallOff.toFixed(3)} → ON ${recallOn.toFixed(3)} ` +
|
|
887
|
+
`${recallRegress ? "**(REGRESSED — gold hardware memory lost)**" : "(no regression)"}`);
|
|
888
|
+
L.push(`- sameScope@5 (hardware in top-5): OFF ${sameOff.toFixed(2)} → ON ${sameOn.toFixed(2)} ` +
|
|
889
|
+
`(${sameOn >= sameOff ? "+" : ""}${(sameOn - sameOff).toFixed(2)})`);
|
|
890
|
+
let verdict;
|
|
891
|
+
if (pass) {
|
|
892
|
+
const magnitude = contamDrop > 0.15
|
|
893
|
+
? "strongly"
|
|
894
|
+
: contamDrop > 0.05
|
|
895
|
+
? "materially"
|
|
896
|
+
: "marginally";
|
|
897
|
+
verdict =
|
|
898
|
+
`**PASS** — soft project affinity ${magnitude} drops cross-scope contamination (−${pct(contamDrop)} absolute) ` +
|
|
899
|
+
`WITHOUT losing same-scope recall. The #203 change is sufficient on this corpus; no hard filter needed.`;
|
|
900
|
+
}
|
|
901
|
+
else if (!contamMaterial) {
|
|
902
|
+
verdict =
|
|
903
|
+
`**FAIL (insufficient)** — contamination did NOT drop materially (−${pct(contamDrop)} absolute). ` +
|
|
904
|
+
`This is a real finding: #203's soft affinity alone is INSUFFICIENT here, and the BM25/FTS-driven cases ` +
|
|
905
|
+
`the architect review predicted for #205 (field-weighted BM25F) likely persist — the marine seed wins on ` +
|
|
906
|
+
`raw token overlap that the additive +0.15 cannot overcome.`;
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
verdict =
|
|
910
|
+
`**FAIL (recall regression)** — contamination dropped (−${pct(contamDrop)} absolute) but a gold hardware ` +
|
|
911
|
+
`memory was LOST when scope engaged (recall@5 ${recallOff.toFixed(3)} → ${recallOn.toFixed(3)}). The ` +
|
|
912
|
+
`affinity is somehow suppressing a same-scope gold result, which should not happen (it is additive only).`;
|
|
913
|
+
}
|
|
914
|
+
L.push(`- ${verdict}\n`);
|
|
915
|
+
return L.join("\n");
|
|
916
|
+
}
|
|
652
917
|
// ---------------------------------------------------------------------------
|
|
653
918
|
// main
|
|
654
919
|
// ---------------------------------------------------------------------------
|
|
@@ -665,14 +930,18 @@ async function main() {
|
|
|
665
930
|
const tmpDir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `hicortex-recall-sweep-${(0, node_crypto_1.randomUUID)().slice(0, 8)}`);
|
|
666
931
|
(0, node_fs_1.mkdirSync)(tmpDir, { recursive: true });
|
|
667
932
|
const dbPath = (0, node_path_1.join)(tmpDir, "sweep.db");
|
|
933
|
+
const scopeDbPath = (0, node_path_1.join)(tmpDir, "scope.db");
|
|
668
934
|
const reportDir = (0, node_path_1.join)(process.cwd(), "data", "eval-recall-sweep");
|
|
669
935
|
console.log(`[recall-sweep] temp DB: ${dbPath}`);
|
|
936
|
+
console.log(`[recall-sweep] scope DB: ${scopeDbPath}`);
|
|
670
937
|
console.log(`[recall-sweep] report dir: ${reportDir}`);
|
|
671
938
|
console.log(`[recall-sweep] ${sessions.filter((s) => s.kind === "focused").length} focused + ` +
|
|
672
939
|
`${sessions.filter((s) => s.kind === "shift").length} shift sessions; ` +
|
|
673
940
|
`${WEIGHTS.length} weights = ${sessions.length * WEIGHTS.length} sweeps x 4 turns = ` +
|
|
674
|
-
`${sessions.length * WEIGHTS.length * 4} retrieves`
|
|
941
|
+
`${sessions.length * WEIGHTS.length * 4} retrieves; ` +
|
|
942
|
+
`+ scope sweep = ${SCOPE_QUERIES.length} queries x 2 modes = ${SCOPE_QUERIES.length * 2} retrieves`);
|
|
675
943
|
let db = null;
|
|
944
|
+
let scopeDb = null;
|
|
676
945
|
try {
|
|
677
946
|
console.log("[recall-sweep] building corpus (embedding 30 memories)...");
|
|
678
947
|
const t0 = Date.now();
|
|
@@ -685,9 +954,20 @@ async function main() {
|
|
|
685
954
|
console.log(`[recall-sweep] sweep done in ${Date.now() - t1}ms (${records.length} turn records)`);
|
|
686
955
|
const focused = summarizeFocused(records, sessions);
|
|
687
956
|
const shifts = summarizeShift(records);
|
|
688
|
-
|
|
957
|
+
let report = renderReport(focused, shifts, records, sessions, {
|
|
689
958
|
memoryCount: CORPUS.length,
|
|
690
959
|
});
|
|
960
|
+
// ---- SCOPE sweep (#203) ----
|
|
961
|
+
console.log(`[recall-sweep] building scope corpus (embedding ${SCOPE_CORPUS.length} project-labeled memories)...`);
|
|
962
|
+
const t2 = Date.now();
|
|
963
|
+
const scopeBuilt = await buildScopeDb(scopeDbPath);
|
|
964
|
+
scopeDb = scopeBuilt.db;
|
|
965
|
+
console.log(`[recall-sweep] scope corpus ready in ${Date.now() - t2}ms (${scopeBuilt.ids.length} memories)`);
|
|
966
|
+
console.log("[recall-sweep] running scope sweep (OFF vs ON)...");
|
|
967
|
+
const t3 = Date.now();
|
|
968
|
+
const scopeRecords = await runScopeSweep(scopeDb, scopeBuilt.idToProject, scopeBuilt.ids);
|
|
969
|
+
console.log(`[recall-sweep] scope sweep done in ${Date.now() - t3}ms (${scopeRecords.length} turn records)`);
|
|
970
|
+
report += "\n\n" + renderScopeSection(scopeRecords);
|
|
691
971
|
(0, node_fs_1.mkdirSync)(reportDir, { recursive: true });
|
|
692
972
|
const reportPath = (0, node_path_1.join)(reportDir, "report.md");
|
|
693
973
|
(0, node_fs_1.writeFileSync)(reportPath, report, "utf-8");
|
|
@@ -701,6 +981,12 @@ async function main() {
|
|
|
701
981
|
}
|
|
702
982
|
catch { /* already closed */ }
|
|
703
983
|
}
|
|
984
|
+
if (scopeDb) {
|
|
985
|
+
try {
|
|
986
|
+
scopeDb.close();
|
|
987
|
+
}
|
|
988
|
+
catch { /* already closed */ }
|
|
989
|
+
}
|
|
704
990
|
if ((0, node_fs_1.existsSync)(tmpDir)) {
|
|
705
991
|
try {
|
|
706
992
|
(0, node_fs_1.rmSync)(tmpDir, { recursive: true, force: true });
|
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
|
-
|
|
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 fetch entries that could change your action — not every shown one. 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: {
|