@futdevpro/fdp-agent-memory 1.1.166 → 1.1.194

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.
Files changed (30) hide show
  1. package/build/package.json +8 -2
  2. package/build/src/_cli/_commands/prune-duplicates.command.js +93 -0
  3. package/build/src/_cli/_commands/retire-temporary.command.js +77 -0
  4. package/build/src/_cli/register-commands.js +4 -0
  5. package/build/src/_collections/config-catalog.const.js +75 -0
  6. package/build/src/_models/data-models/fam-entry.data-model.js +6 -0
  7. package/build/src/_models/data-models/fam-memory.data-model.js +3 -0
  8. package/build/src/_modules/embedding/_collections/fam-exact-duplicate-prune.util.js +85 -0
  9. package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +32 -0
  10. package/build/src/_modules/embedding/_services/fam-entry.data-service.js +21 -0
  11. package/build/src/_modules/embedding/_services/fam-exact-duplicate-prune.control-service.js +152 -0
  12. package/build/src/_modules/embedding/index.js +7 -1
  13. package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +21 -1
  14. package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +55 -1
  15. package/build/src/_modules/ingest/_services/fam-scan-scheduler.control-service.js +26 -0
  16. package/build/src/_modules/mcp/_collections/fam-core-tools.const.js +11 -0
  17. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +2 -0
  18. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +29 -7
  19. package/build/src/_modules/retrieval/_collections/fam-cross-table-note.util.js +136 -0
  20. package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +75 -2
  21. package/build/src/_modules/retrieval/_collections/fam-literal-phrase.util.js +74 -0
  22. package/build/src/_modules/retrieval/_collections/fam-memory-retention.util.js +70 -0
  23. package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +8 -1
  24. package/build/src/_modules/retrieval/_services/fam-memory-retention.control-service.js +118 -0
  25. package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +33 -0
  26. package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +224 -5
  27. package/build/src/_modules/retrieval/index.js +6 -1
  28. package/build/src/_routes/server/api/api.controller.js +77 -15
  29. package/package.json +8 -2
  30. package/build/src/_integration-tests/_helpers/fam-integration-test-setup.util.js +0 -105
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * MEMÓRIA-RETENCIÓ — az alvó beszélgetés-törmelék nyugdíjazásának döntési logikája (2026-09-02).
4
+ *
5
+ * **A probléma (mérve).** Az aktív `memory` tár 20,2%-a (8 717 bejegyzés) nyers user-prompt
6
+ * (`kind: user_prompt` / `tags: [temporary_context]`) — egy egyszeri, 2026-06-os claude-mem import
7
+ * hagyatéka (azóta 4 új keletkezett, tehát NINCS élő csap). **98,2%-uk soha nem lett felidézve**, mégis
8
+ * teljes súllyal versenyeznek a gondozott jegyzetekkel a nyers koszinusz-rangsorban — és mivel rövidek
9
+ * és tematikusak, gyakran nyernek is.
10
+ *
11
+ * **A szabály (v2, owner: „javítsd, amit kell" — 2026-09-02):** a törmelék-osztály 30+ nap után nyugdíjba
12
+ * vonul, a felidézés-számtól FÜGGETLENÜL.
13
+ *
14
+ * **Miért nem véd a `recallCount` ezen az osztályon — mérve.** Az első változat a „amit többet használtunk,
15
+ * az tovább él" elv szerint kivételezett a valaha felidézettekkel (164 túlélő). Az audit ezt megbuktatta:
16
+ * a legtöbbet „felidézett" túlélők **gépi zaj** voltak — `<task-notification>` és `<ide_opened_file>`
17
+ * XML-blokkok, recall=281-gyel a csúcson (67/164 tisztán ilyen; a maradék átirat-üledék: „Látod", „Azt",
18
+ * build-logok). A felidézés AUTOMATIKUS (minden visszaadott találat recallCount++-t kap), így ezen az
19
+ * osztályon a szám a RANGSOR-KITETTSÉGET méri, nem a használatot — és öngerjesztő: ami magasra rangsorol,
20
+ * azt a visszaadás „felidézi", amitől védetté válik. Az owner elve a MEMÓRIÁKRA vonatkozik; a nyers
21
+ * átirat-törmelék nem az. A nyugdíj ettől még NEM törlés: minden hidegen kereshető marad, és
22
+ * visszafordítható — a genuinly hasznos darab a cold-search-ben él, vagy gondozott bejegyzésként újraírható.
23
+ *
24
+ * Ugyanezen mérés miatt a REAKTIVÁCIÓ is kihagyja ezt az osztályt (`selectMemoryIds` szűrője) — a hurok
25
+ * motorja leállt: a törmelék többé nem tud „használtnak" látszani attól, hogy előkerül.
26
+ *
27
+ * **A nyugdíj NEM törlés:** `superseded: true` + `retiredAt` — a tartalom és a vektor a poolban marad,
28
+ * a `memory.supersededPenalty` (0.25) süllyeszti, a cold-search megtalálja. Visszafordítható.
29
+ *
30
+ * **Hatókör-korlátok (mind mért indokkal):**
31
+ * - CSAK a törmelék-osztály (`user_prompt` / `temporary_context`) — a `session-observation` NEM az:
32
+ * 24%-a felidézett és ma is aktívan nő; a `memory_entry` a gondozott törzs.
33
+ * - CSAK a 30+ naposak (`memory.retireTemporaryAfterDays`) — a friss kontextus a session alatt még
34
+ * ténylegesen kontextus; utána üledék.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.FAM_MemoryRetention_Util = void 0;
38
+ /** A törmelék-osztály definíciója — UGYANAZ, amit a kereszt-tár szonda kizár (egy definíció, két felhasználás). */
39
+ const DEBRIS_KINDS = ['user_prompt'];
40
+ const DEBRIS_TAGS = ['temporary_context'];
41
+ const DAY_MS = 24 * 60 * 60 * 1000;
42
+ class FAM_MemoryRetention_Util {
43
+ /** Törmelék-osztályú-e a bejegyzés (nyers prompt / átmeneti kontextus). */
44
+ static isDebris(candidate) {
45
+ if (candidate.kind && DEBRIS_KINDS.includes(candidate.kind)) {
46
+ return true;
47
+ }
48
+ return (candidate.tags ?? []).some((tag) => DEBRIS_TAGS.includes(tag));
49
+ }
50
+ /**
51
+ * A döntés egyetlen bejegyzésre. A sorrend a VÉDELMI sorrend: előbb a kizáró okok (nem törmelék /
52
+ * már nyugdíjas / túl friss), és csak ha egyik sem áll, akkor `retire`. A `recallCount` ITT SZÁNDÉKOSAN
53
+ * nem véd — az indoklás (mért öngerjesztő hurok) a fájl fejlécében.
54
+ */
55
+ static decide(candidate, set) {
56
+ if (!FAM_MemoryRetention_Util.isDebris(candidate)) {
57
+ return 'keep-not-debris';
58
+ }
59
+ if (candidate.superseded === true) {
60
+ return 'keep-already-superseded';
61
+ }
62
+ const createdAt = typeof candidate.createdAt === 'number' ? candidate.createdAt : Number.NaN;
63
+ // Datum nelkuli bejegyzest NEM nyugdijazunk — a hianyzo teny nem bizonyitek az oregsegre.
64
+ if (!Number.isFinite(createdAt) || set.now - createdAt < set.retireAfterDays * DAY_MS) {
65
+ return 'keep-too-young';
66
+ }
67
+ return 'retire';
68
+ }
69
+ }
70
+ exports.FAM_MemoryRetention_Util = FAM_MemoryRetention_Util;
@@ -7,6 +7,7 @@ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
7
7
  const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
8
8
  const embedding_1 = require("../../embedding");
9
9
  const fam_memory_data_model_1 = require("../../../_models/data-models/fam-memory.data-model");
10
+ const fam_memory_retention_util_1 = require("../_collections/fam-memory-retention.util");
10
11
  /**
11
12
  * `FAM_MemoryReactivation_ControlService` (dsgn-013 §4, MAM F2) — a memória FELIDÉZÉS-megerősítése. Amikor a `read`
12
13
  * ténylegesen VISSZAAD egy `memory`-hitet (a felhasználó/agent felidézte), az „új energiát kap": `lastRecalledAt = now`,
@@ -55,7 +56,13 @@ class FAM_MemoryReactivation_ControlService {
55
56
  */
56
57
  static selectMemoryIds(hits) {
57
58
  const ids = hits
58
- .filter((hit) => hit.table === fam_table_type_enum_1.FAM_Table.memory && typeof hit.id === 'string' && hit.id.length > 0)
59
+ .filter((hit) => hit.table === fam_table_type_enum_1.FAM_Table.memory && typeof hit.id === 'string' && hit.id.length > 0
60
+ // A TORMELEK-osztaly (user_prompt / temporary_context) NEM kap felidezes-megerositest
61
+ // (2026-09-02, mert ongerjeszto hurok): a rovid atirat-toredek magasra rangsorol -> a
62
+ // visszaadas automatikus recallCount++-t adott -> "hasznaltnak" latszott es a retencio
63
+ // vedte. A csucson recall=281 egy <task-notification> XML-blokkon. A szamlalo ezen az
64
+ // osztalyon a rangsor-kitettseget merte, nem a hasznalatot — a motor itt all le.
65
+ && !fam_memory_retention_util_1.FAM_MemoryRetention_Util.isDebris({ kind: hit.kind, tags: hit.tags }))
59
66
  .map((hit) => hit.id);
60
67
  return Array.from(new Set(ids));
61
68
  }
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_MemoryRetention_ControlService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
6
+ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
7
+ const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
8
+ const embedding_1 = require("../../embedding");
9
+ const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
10
+ const fam_memory_retention_util_1 = require("../_collections/fam-memory-retention.util");
11
+ /**
12
+ * MEMÓRIA-RETENCIÓ (2026-09-02) — az alvó beszélgetés-törmelék heti nyugdíjazása. A döntési logika és a
13
+ * teljes indoklás a pure utilban (`fam-memory-retention.util.ts`); ez a réteg a Mongo-munka.
14
+ *
15
+ * A nyugdíj = `superseded: true` + `retiredAt` — NEM törlés: a tartalom és a vektor a poolban marad
16
+ * (a `memory.supersededPenalty` süllyeszti, a cold-search megtalálja). A pool-hoz ezért NEM nyúlunk
17
+ * (nincs `removeVector`) — pontosan úgy, ahogy a tartalom-leváltásos `superseded` ág sem teszi.
18
+ *
19
+ * **Heti automatika:** a scan-scheduler a heti re-scan UTÁN hívja (`memory.retireTemporaryEnabled`,
20
+ * default true) — a retenció lényege az automatizmus, különben a törmelék sosem vonul nyugdíjba.
21
+ */
22
+ class FAM_MemoryRetention_ControlService {
23
+ static _instance;
24
+ static getInstance() {
25
+ if (!FAM_MemoryRetention_ControlService._instance) {
26
+ FAM_MemoryRetention_ControlService._instance = new FAM_MemoryRetention_ControlService();
27
+ }
28
+ return FAM_MemoryRetention_ControlService._instance;
29
+ }
30
+ /** A retenció futtatása — `dryRun` esetén CSAK mér, semmit nem ír. */
31
+ async retire(input = {}) {
32
+ const dryRun = input.dryRun !== false;
33
+ const retireAfterDays = typeof input.retireAfterDays === 'number' && input.retireAfterDays > 0
34
+ ? Math.floor(input.retireAfterDays)
35
+ : await this.resolveRetireAfterDays();
36
+ const registryEntry = embedding_1.FAM_StoreRegistry_Util.getEntry(fam_table_type_enum_1.FAM_Table.memory);
37
+ if (!registryEntry) {
38
+ throw new Error('A memory tár nincs a store-registry-ben — a retenció nem futtatható.');
39
+ }
40
+ // A példányosítás regisztrálja a mongoose-modellt (a raw collection eléréséhez) — a bevett minta.
41
+ new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: 'memory-retention' });
42
+ const collection = mongoose_1.default.models[registryEntry.dataParams.dataName]?.collection;
43
+ if (!collection) {
44
+ throw new Error('A memory collection nem elérhető — a retenció nem futtatható.');
45
+ }
46
+ // CSAK a törmelék-osztály kerül a jelölt-halmazba (a Mongo szűr, nem mi töltünk fölöslegesen) —
47
+ // a szűkítés a pure util definícióját tükrözi; a végső döntést bejegyzésenként a util hozza.
48
+ const raw = await collection.find({
49
+ _deleted: null,
50
+ $or: [{ kind: 'user_prompt' }, { tags: 'temporary_context' }],
51
+ }, { projection: { kind: 1, tags: 1, recallCount: 1, superseded: 1, __created: 1 } }).toArray();
52
+ const now = Date.now();
53
+ const toRetire = [];
54
+ let keptTooYoung = 0;
55
+ let alreadySuperseded = 0;
56
+ for (const row of raw) {
57
+ const candidate = {
58
+ id: String(row._id),
59
+ kind: row.kind,
60
+ tags: row.tags,
61
+ recallCount: row.recallCount,
62
+ superseded: row.superseded,
63
+ createdAt: row.__created instanceof Date ? row.__created.getTime() : undefined,
64
+ };
65
+ const verdict = fam_memory_retention_util_1.FAM_MemoryRetention_Util.decide(candidate, { now: now, retireAfterDays: retireAfterDays });
66
+ if (verdict === 'retire') {
67
+ toRetire.push(row._id);
68
+ }
69
+ else if (verdict === 'keep-too-young') {
70
+ keptTooYoung++;
71
+ }
72
+ else if (verdict === 'keep-already-superseded') {
73
+ alreadySuperseded++;
74
+ }
75
+ // `keep-not-debris` a Mongo-szűrő miatt nem fordulhat elő — ha mégis, az csendben kimarad
76
+ // a számlálókból, és a `scanned - összeg` eltérés jelzi a szűrő-drift-et.
77
+ }
78
+ if (!dryRun && toRetire.length) {
79
+ await collection.updateMany({ _id: { $in: toRetire } }, { $set: { superseded: true, retiredAt: now } });
80
+ fsm_dynamo_1.DyFM_Log.log(`[memory-retention] ${toRetire.length} alvo tormelek-bejegyzes nyugdijazva `
81
+ + `(superseded+retiredAt; ${keptTooYoung} meg fiatal — a recallCount ezen az osztalyon nem ved, `
82
+ + 'mert ongerjeszto rangsor-kitettseget mert).');
83
+ }
84
+ return {
85
+ operation: 'retire_temporary_memories',
86
+ dryRun: dryRun,
87
+ retireAfterDays: retireAfterDays,
88
+ scanned: raw.length,
89
+ retired: toRetire.length,
90
+ keptTooYoung: keptTooYoung,
91
+ alreadySuperseded: alreadySuperseded,
92
+ };
93
+ }
94
+ /** A `memory.retireTemporaryAfterDays` feloldása (hiba/hiány → 30 — a retenció sosem bukik configon). */
95
+ async resolveRetireAfterDays() {
96
+ try {
97
+ const value = (await config_control_service_1.FAM_Config_ControlService.getInstance()
98
+ .resolve('memory.retireTemporaryAfterDays', { table: fam_table_type_enum_1.FAM_Table.memory })).value;
99
+ const num = typeof value === 'number' ? value : Number(value);
100
+ return Number.isFinite(num) && num > 0 ? Math.floor(num) : 30;
101
+ }
102
+ catch {
103
+ return 30;
104
+ }
105
+ }
106
+ /** Engedélyezett-e a heti automatika (`memory.retireTemporaryEnabled`, default true). */
107
+ async isAutoEnabled() {
108
+ try {
109
+ const value = (await config_control_service_1.FAM_Config_ControlService.getInstance()
110
+ .resolve('memory.retireTemporaryEnabled', { table: fam_table_type_enum_1.FAM_Table.memory })).value;
111
+ return value !== false;
112
+ }
113
+ catch {
114
+ return true;
115
+ }
116
+ }
117
+ }
118
+ exports.FAM_MemoryRetention_ControlService = FAM_MemoryRetention_ControlService;
@@ -56,6 +56,39 @@ class FAM_RetrievalCandidate_DataService {
56
56
  filter.referenceCodes = { $in: codes };
57
57
  return this.findActiveCandidates(registryEntry, filter);
58
58
  }
59
+ /**
60
+ * SZÓ SZERINTI IDÉZET-MENTÉS (2026-09-02): a `phrase`-t szó szerint tartalmazó aktív entry-k.
61
+ *
62
+ * **Miért kell:** mérve, a bemásolt hibaüzenetet hordozó chunk BE SEM KERÜL a vektor-jelöltek közé
63
+ * (60 találatból 0 tartalmazta a keresett stringet) — ez recall-hiba, amit rangsorolással nem lehet
64
+ * javítani. A minta a FEAT-003 kód-expanzió: determinisztikus, pontos egyezés a vektor-úton KÍVÜL.
65
+ *
66
+ * **MÉRET-KAPU (kötelező):** a `content` substring-keresés NEM indexelhető → collection-scan. Mérve:
67
+ * `rules` 296 ms · `coding_patterns` 504 ms · `knowledge` 2,1 s · `memory` 4,9 s · `documents` 13,1 s ·
68
+ * `codebase` 45,2 s. A `maxDocs` fölötti tár **kihagyva** (üres lista) — a mentés SOHA nem tehet egy
69
+ * olvasást másodperces műveletté. A darabszám `estimatedDocumentCount` (collection-metaadat, ~0 ms).
70
+ */
71
+ async loadByLiteralPhrase(input, regexSource, set) {
72
+ const registryEntry = embedding_1.FAM_StoreRegistry_Util.getEntry(input.table);
73
+ if (!registryEntry || !regexSource) {
74
+ return [];
75
+ }
76
+ const model = mongoose_1.default.models[registryEntry.dataParams.dataName];
77
+ if (!model) {
78
+ return [];
79
+ }
80
+ const size = await model.collection.estimatedDocumentCount();
81
+ if (size > set.maxDocs) {
82
+ return [];
83
+ }
84
+ const filter = this.buildFilter(input, registryEntry);
85
+ filter.content = { $regex: regexSource, $options: 'i' };
86
+ const rows = await model.collection
87
+ .find({ ...filter, _deleted: null }, { projection: { contentVector: 0 } })
88
+ .limit(set.limit)
89
+ .toArray();
90
+ return rows;
91
+ }
59
92
  /**
60
93
  * A topK-hit metaadat-betöltése `_id`-listából (az UNSCOPED read-fast-path-hez, SP-5.x perf). A főpool-
61
94
  * keresés UTÁN CSAK a tényleges hit-eket tölti (a hit-row + weight + recency összefűzéséhez), NEM mind a
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_Retrieval_ControlService = exports.MAX_PAGE_SIZE = void 0;
4
+ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
+ const fam_cross_table_note_util_1 = require("../_collections/fam-cross-table-note.util");
4
6
  const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
5
7
  const fam_reference_code_util_1 = require("../../../_collections/fam-reference-code.util");
6
8
  const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
@@ -10,6 +12,7 @@ const fam_retrieval_candidate_data_service_1 = require("./fam-retrieval-candidat
10
12
  const fam_retrieval_suggestions_util_1 = require("./fam-retrieval-suggestions.util");
11
13
  const fam_memory_activation_util_1 = require("../_collections/fam-memory-activation.util");
12
14
  const fam_lexical_match_util_1 = require("../_collections/fam-lexical-match.util");
15
+ const fam_literal_phrase_util_1 = require("../_collections/fam-literal-phrase.util");
13
16
  const fam_result_diversity_util_1 = require("../_collections/fam-result-diversity.util");
14
17
  const fam_memory_reactivation_control_service_1 = require("./fam-memory-reactivation.control-service");
15
18
  const fam_memory_cold_search_control_service_1 = require("./fam-memory-cold-search.control-service");
@@ -98,7 +101,12 @@ class FAM_Retrieval_ControlService {
98
101
  // FEAT-006 context-injection deduper: a hívó által MÁR injektált `_id`-k kizárása (a minScore/topK ELŐTT,
99
102
  // hogy ne foglaljanak helyet) — egy session ne kapja vissza ugyanazt a kontextust kétszer. A kizárás a
100
103
  // kód-exact-re IS vonatkozik (ha a session már birtokolja, nem kell újra, F3-1 ellenére sem).
101
- const visibleHits = this.excludeSeen(mergedHits, query.excludeIds);
104
+ // ④c SZÓ SZERINTI IDÉZET-MENTÉS (2026-09-02): ha a query idézet-szerű, a pontosan egyező entry-k a
105
+ // vektor-úton KÍVÜL is bekerülnek. Mérve: a bemásolt hibaüzenet chunkja BE SEM KERÜLT a jelöltek
106
+ // közé (60/0), tehát ez recall-hiba — rangsorolással nem javítható. Ugyanaz a minta, mint a
107
+ // FEAT-003 kód-expanzió; a már bent lévő hitek NEM változnak, csak a hiányzók egészülnek ki.
108
+ const rescuedHits = await this.applyLiteralRescue({ query, config, scopeExpand, hits: mergedHits, includeContent });
109
+ const visibleHits = this.excludeSeen(rescuedHits, query.excludeIds);
102
110
  // ⑤ weight-szorzó MÁR a hit-eken (finalScore). ⑥ minScore-küszöb → relevantSet — DE F3-2: a kód-exact
103
111
  // citáció (`matchedCodes` nem-üres) BYPASS-olja a minScore-t (a determinisztikusan citált kód eleme
104
112
  // releváns a score-tól függetlenül; a `weight≤0` továbbra is kizárt, mert az már a merge ELŐTT kiesik).
@@ -112,11 +120,108 @@ class FAM_Retrieval_ControlService {
112
120
  const dedupedSet = FAM_Retrieval_ControlService.dedupeByContentHash(relevantSet);
113
121
  // ⑧ topK-vágás + dense-detektálás + suggestions.
114
122
  const result = this.buildResult({ query, relevantSet: dedupedSet, config, scopeExpand, detectedCodes });
123
+ // ⑨ KERESZT-TÁR „mellesleg"-javaslat (2026-09-01): a `hits` MÁR KÉSZ, és NEM változik tőle. Csak akkor
124
+ // fut, ha a hívó NEM kérte a `memory`/`knowledge` tárat (különben már benne lenne a listában).
125
+ const probeOutcome = await this.probeCrossTable({ query, queryVector, scopeExpand, config, result });
126
+ if (probeOutcome?.note) {
127
+ result.crossTableNote = probeOutcome.note;
128
+ }
129
+ if (query.includeCrossTableProbe && probeOutcome?.debug) {
130
+ result.crossTableProbe = probeOutcome.debug;
131
+ }
115
132
  // dsgn-013 §4 (MAM F2) — FELIDÉZÉS-megerősítés: a ténylegesen VISSZAADOTT `memory`-hitek „új energiát kapnak"
116
133
  // (lastRecalledAt=now, recallCount++, dormant=false). ASYNC fire-and-forget — NEM blokkolja a read választ.
117
- this.reactivateReturnedMemoryHits(result.hits, config);
134
+ if (!query.skipReactivation) {
135
+ this.reactivateReturnedMemoryHits(result.hits, config);
136
+ }
118
137
  return result;
119
138
  }
139
+ /**
140
+ * A kereszt-tár szonda: EGY olcsó keresés a `memory` + `knowledge` táron, hogy kiderüljön, van-e ott
141
+ * ÉRDEMBEN erősebb találat. A döntés a tiszta `FAM_CrossTableNote_Util`-ban van (ott áll a kalibráció
142
+ * és a korlátok indoklása is); ez a metódus csak a jelölteket szerzi be.
143
+ *
144
+ * **Nem fut**, ha ki van kapcsolva, ha a küszöb 0, vagy ha a hívó ELEVE kérte ezeket a tárakat — akkor
145
+ * a találat már a listában van, és egy „mellesleg ott is van" mondat csak zaj lenne.
146
+ *
147
+ * **Hibatűrő:** a szonda MELLÉK-szolgáltatás. Ha bármi elromlik benne, a read válasza attól még teljes
148
+ * — némán elhagyjuk a jegyzetet, nem buktatjuk a hívást.
149
+ */
150
+ async probeCrossTable(set) {
151
+ // A DIAGNOSZTIKA kérése önmagában is ok a futásra: enélkül a kikapcsolt szonda mérhetetlen lenne.
152
+ if ((!set.config.crossTableNoteEnabled || !(set.config.crossTableNoteMinDelta > 0))
153
+ && !set.query.includeCrossTableProbe) {
154
+ return undefined;
155
+ }
156
+ const asked = new Set(set.query.tables);
157
+ const probeTables = set.config.crossTableNoteProbeTables
158
+ .filter((table) => !asked.has(table));
159
+ if (!probeTables.length) {
160
+ return undefined;
161
+ }
162
+ try {
163
+ // A tárak szondázása PÁRHUZAMOS. Mérve: sorosan +713 ms (+68,6%) minden read-en, ami egy
164
+ // 6,8%-os megszólalási arányhoz mérve elfogadhatatlan ár; a két keresés független egymástól.
165
+ const perTable = await Promise.all(probeTables.map((table) => this.probeTable(table, set)));
166
+ const candidates = perTable.flat();
167
+ // Az összevetés alapja a NYERS koszinusz — a `finalScore` tár-függő szorzókat hordoz (weight, decay,
168
+ // projekt-súly), így két tár között nem összemérhető.
169
+ const primaryTopScore = set.result.hits.reduce((top, hit) => (hit.score > top ? hit.score : top), 0);
170
+ // A JEGYZET csak akkor keletkezik, ha a funkció TÉNYLEGESEN be van kapcsolva. A diagnosztika
171
+ // (`includeCrossTableProbe`) megengedi a szonda LEFUTÁSÁT kikapcsolt állapotban is — de egy
172
+ // mérő-kapcsoló SOHA nem hozhat vissza egy kikapcsolt funkciót a válaszba.
173
+ const enabled = set.config.crossTableNoteEnabled && set.config.crossTableNoteMinDelta > 0;
174
+ const note = enabled ? fam_cross_table_note_util_1.FAM_CrossTableNote_Util.decide({
175
+ primaryTopScore: primaryTopScore,
176
+ candidates: candidates,
177
+ minDelta: set.config.crossTableNoteMinDelta,
178
+ minScore: set.config.crossTableNoteMinScore,
179
+ }) : undefined;
180
+ return { note: note, debug: fam_cross_table_note_util_1.FAM_CrossTableNote_Util.describeProbe(primaryTopScore, candidates, Boolean(note)) };
181
+ }
182
+ catch (error) {
183
+ fsm_dynamo_1.DyFM_Log.warn(`[read] a kereszt-tar szonda hibara futott, a jegyzet elmarad (a valasz teljes): ${String(error)}`);
184
+ return undefined;
185
+ }
186
+ }
187
+ /**
188
+ * EGY szonda-tár lekérdezése — a jelöltekké alakított találatokkal.
189
+ *
190
+ * A configot a SZONDÁZOTT tárra oldjuk fel, NEM az elsődlegesre. Ez nem finomhangolás: a `memory`
191
+ * rangsorát a saját decay-/superseded-/cold-search-beállításai alakítják, és ezek per-tár defaultokkal
192
+ * bírnak. Az elsődleges tár configjával szondázva MÁS jelölt-halmazt kapunk, mint amit ugyanaz a kérdés
193
+ * egy közvetlen `memory`-kereséssel adna — a mérés és a működés más eredményre jut, és a kalibráció
194
+ * olyan számra épülne, ami sehol nem érvényes. (Mérve: 5/44 vs 2/44 megszólalás ugyanazon a 44 eseten.)
195
+ */
196
+ async probeTable(table, set) {
197
+ const tableConfig = await this.resolveReadConfig(table, set.query);
198
+ // A szonda SAJÁT, kicsi topK-val fut — nem második találati listát épít, csak eldönti, van-e erősebb.
199
+ // A cold-search a KÖZVETLEN memória-kérdések dormant-mentőöve; egy mellék-jegyzet szondájának a
200
+ // forró pool elég — a superseded/dormant jelöltet úgyis kizárnánk. A szonda 3-as topK-jánál a
201
+ // `strongHotHits < topK` kapu szinte mindig nyitna, és a dormant-Mongo-munka a +514 ms része volt.
202
+ const probeConfig = {
203
+ ...tableConfig, topK: set.config.crossTableNoteTopK, minScore: 0,
204
+ memoryColdSearchEnabled: false,
205
+ };
206
+ const hits = await this.searchTable({
207
+ table: table, query: set.query, queryVector: set.queryVector,
208
+ scopeExpand: set.scopeExpand, config: probeConfig, includeContent: false,
209
+ // 4× fejtér a superseded/törmelék/`weight<=0` szűrésnek — 500 helyett 12 betöltött entry.
210
+ vectorTopK: Math.max(12, set.config.crossTableNoteTopK * 4),
211
+ });
212
+ return hits.slice(0, set.config.crossTableNoteTopK).map((hit) => {
213
+ // A `kind`/`tags`/`scope` NEM dekoráció: az MCP-n ÍRT memória-bejegyzésnek nincs fájl-útja
214
+ // (mérve: 21 jelöltből 16-nak), és ott a nyers ObjectId semmit nem mond a hívónak.
215
+ const leafScope = hit.scopePath?.[hit.scopePath.length - 1];
216
+ return {
217
+ id: hit.id, table: hit.table, score: hit.score,
218
+ sourceFilePath: hit.sourceFilePath, headingPath: hit.headingPath,
219
+ kind: hit.kind, tags: hit.tags,
220
+ scope: leafScope?.canonicalName,
221
+ superseded: hit.superseded,
222
+ };
223
+ });
224
+ }
120
225
  /**
121
226
  * A visszaadott top-K `memory`-hitek async felidézés-megerősítése (dsgn-013 §4). Fire-and-forget (void): a read
122
227
  * már elkészült, a felidézés-frissítés best-effort. Csak a `memory`-táras, `_id`-vel bíró hitre; üres → no-op.
@@ -189,6 +294,57 @@ class FAM_Retrieval_ControlService {
189
294
  }
190
295
  return Array.from(byId.values());
191
296
  }
297
+ /**
298
+ * SZÓ SZERINTI IDÉZET-MENTÉS. A `FAM_LiteralPhrase_Util` dönti el, hogy a query idézet-szerű-e (a
299
+ * kritériumok és a mért indoklás ott); ha igen, a pontosan egyező entry-ket a data-service hozza be a
300
+ * MÉRET-KAPU betartásával. A már meglévő hitek ÉRINTETLENEK — a mentés csak kiegészít.
301
+ *
302
+ * A mentett entry `score`-ja NEM 1,0 (az a kód-expanzió exact-citation szintje): egy szó szerinti
303
+ * mondat-egyezés erős, de nem azonos egy explicit azonosító-citációval. A `LITERAL_RESCUE_SCORE` a
304
+ * `relevanceFloor` fölé teszi őket, a rangsorban pedig a szokásos szorzók (weight, lexikai boost —
305
+ * beleértve az idézet-bónuszt) döntenek. Így a mentés **felszínre hoz**, de nem **tol félre**.
306
+ *
307
+ * Hibatűrő: a mentés MELLÉK-szolgáltatás — ha elszáll, a válasz a vektor-találatokkal teljes.
308
+ */
309
+ async applyLiteralRescue(set) {
310
+ if (!set.config.literalRescueEnabled) {
311
+ return set.hits;
312
+ }
313
+ const literal = fam_literal_phrase_util_1.FAM_LiteralPhrase_Util.extract(set.query.query);
314
+ if (!literal) {
315
+ return set.hits;
316
+ }
317
+ try {
318
+ const byId = new Map();
319
+ for (const hit of set.hits) {
320
+ byId.set(hit.id, hit);
321
+ }
322
+ const regexSource = fam_literal_phrase_util_1.FAM_LiteralPhrase_Util.toRegexSource(literal.phrase);
323
+ for (const table of Array.from(new Set(set.query.tables))) {
324
+ const entries = await new fam_retrieval_candidate_data_service_1.FAM_RetrievalCandidate_DataService().loadByLiteralPhrase({
325
+ table: table,
326
+ scopeIdSet: set.scopeExpand.scopeIdSet,
327
+ tagFilter: set.query.tagFilter,
328
+ kindFilter: set.query.kindFilter,
329
+ }, regexSource, { maxDocs: set.config.literalRescueMaxDocs, limit: set.config.literalRescueLimit });
330
+ for (const entry of entries) {
331
+ const id = entry._id ? String(entry._id) : undefined;
332
+ const weight = entry.weight ?? 1;
333
+ // A `weight<=0` kizárás KONZISZTENS a vektor-ággal — a mentés nem kerülheti meg a tiltást.
334
+ if (!id || weight <= 0 || byId.has(id)) {
335
+ continue;
336
+ }
337
+ byId.set(id, this.toLiteralRescueHit({ entry, id, table, weight, includeContent: set.includeContent }));
338
+ }
339
+ }
340
+ return Array.from(byId.values());
341
+ }
342
+ catch (error) {
343
+ fsm_dynamo_1.DyFM_Log.warn(`[read] a szo szerinti idezet-mentes hibara futott, a valasz a vektor-talalatokkal `
344
+ + `teljes: ${String(error)}`);
345
+ return set.hits;
346
+ }
347
+ }
192
348
  /**
193
349
  * Egy kód-citáló entry beolvasztása a hit-halmazba (FEAT-003). `score=1.0` (exact citation = maximális
194
350
  * relevancia), `finalScore=1.0×weight` (a meglévő weight-modellbe illesztve). `weight≤0` → kizárt
@@ -221,6 +377,37 @@ class FAM_Retrieval_ControlService {
221
377
  set.byId.set(id, this.toCodeHit({ entry: set.entry, id, table: set.table, weight, matched, includeContent: set.includeContent }));
222
378
  }
223
379
  /** Egy kód-citáló entry → `FAM_RetrievalHit` (exact citation: `score=1.0`, `matchedCodes` kitöltve). */
380
+ /**
381
+ * A mentett entry `score`-ja. SZÁNDÉKOSAN a `relevanceFloor` (0,60) FÖLÖTT, de a kód-expanzió
382
+ * exact-citation 1,0-ja ALATT: a szó szerinti mondat-egyezés erős jel, de nem azonos egy explicit
383
+ * azonosító-citációval. Így a mentett elem biztosan látszik, de nem söpri le a valódi top-találatokat.
384
+ */
385
+ static LITERAL_RESCUE_SCORE = 0.75;
386
+ /** Egy szó szerint egyező entry → `FAM_RetrievalHit` (idézet-mentés; a `matchedCodes` üres marad). */
387
+ toLiteralRescueHit(set) {
388
+ const lastModified = set.entry.__lastModified;
389
+ const score = FAM_Retrieval_ControlService.LITERAL_RESCUE_SCORE;
390
+ return {
391
+ id: set.id,
392
+ table: set.table,
393
+ score: score,
394
+ weight: set.weight,
395
+ finalScore: score * set.weight,
396
+ content: set.includeContent ? set.entry.content : undefined,
397
+ contentHash: set.entry.contentHash,
398
+ kind: set.entry.kind,
399
+ tags: set.entry.tags ?? [],
400
+ ...this.ruleMeta(set.entry, set.table),
401
+ scopePath: (set.entry.scopePath ?? []),
402
+ source: set.entry.source,
403
+ sourceFilePath: set.entry.sourceFilePath ?? null,
404
+ absolutePath: set.entry.source?.absolutePath ?? null,
405
+ chunkType: set.entry.chunkType,
406
+ headingPath: set.entry.headingPath ?? [],
407
+ position: set.entry.position,
408
+ lastModifiedMs: lastModified ? new Date(lastModified).getTime() : 0,
409
+ };
410
+ }
224
411
  toCodeHit(set) {
225
412
  const lastModified = set.entry.__lastModified;
226
413
  return {
@@ -276,7 +463,7 @@ class FAM_Retrieval_ControlService {
276
463
  vectorHits = await embedding_1.FAM_VectorSearch_ControlService.getInstance().search({
277
464
  table: set.table,
278
465
  queryVector: set.queryVector,
279
- topK: exports.MAX_PAGE_SIZE,
466
+ topK: set.vectorTopK ?? exports.MAX_PAGE_SIZE,
280
467
  });
281
468
  if (!vectorHits.length) {
282
469
  return [];
@@ -312,12 +499,15 @@ class FAM_Retrieval_ControlService {
312
499
  table: set.table,
313
500
  queryVector: set.queryVector,
314
501
  candidateIds: candidateIds,
315
- topK: Math.min(candidateIds.length, exports.MAX_PAGE_SIZE),
502
+ topK: Math.min(candidateIds.length, set.vectorTopK ?? exports.MAX_PAGE_SIZE),
316
503
  });
317
504
  }
318
505
  // ⑤ weight-szorzó: finalScore = score × (weight ?? 1); weight ≤ 0 → kizárt (nem kerül a hits közé).
319
506
  // Improvement 2: a query lexikai term-jeit EGYSZER tokenizáljuk (a hibrid keyword-boost-hoz a toHit-ban).
320
507
  const queryTerms = fam_lexical_match_util_1.FAM_LexicalMatch_Util.tokenize(set.query.query);
508
+ // A szó szerinti idézet-egyezéshez a NYERS, sorrendhelyes token-sorozat kell (stopwordökkel együtt) —
509
+ // EGYSZER számoljuk ki a query-re, nem hit-enként.
510
+ const queryPhraseTokens = fam_lexical_match_util_1.FAM_LexicalMatch_Util.tokenizeSequence(set.query.query);
321
511
  const hits = [];
322
512
  for (const vectorHit of vectorHits) {
323
513
  const entry = byId.get(vectorHit.id);
@@ -330,6 +520,7 @@ class FAM_Retrieval_ControlService {
330
520
  }
331
521
  hits.push(this.toHit({
332
522
  entry, vectorHit, table: set.table, weight, queryTerms: queryTerms,
523
+ queryPhraseTokens: queryPhraseTokens,
333
524
  importChunkWeight: set.config.importChunkWeight, importSourceWeight: set.config.importSourceWeight,
334
525
  includeContent: set.includeContent, config: set.config, nowMs: Date.now(),
335
526
  }));
@@ -363,6 +554,7 @@ class FAM_Retrieval_ControlService {
363
554
  });
364
555
  const seen = new Set(hits.map((hit) => hit.id));
365
556
  const queryTerms = fam_lexical_match_util_1.FAM_LexicalMatch_Util.tokenize(query.query);
557
+ const queryPhraseTokens = fam_lexical_match_util_1.FAM_LexicalMatch_Util.tokenizeSequence(query.query);
366
558
  for (const cold of coldHits) {
367
559
  const id = cold.entry._id;
368
560
  const weight = cold.entry.weight ?? 1;
@@ -371,6 +563,7 @@ class FAM_Retrieval_ControlService {
371
563
  }
372
564
  hits.push(this.toHit({
373
565
  entry: cold.entry, vectorHit: { id: id, score: cold.score }, table: set.table, weight: weight, queryTerms: queryTerms,
566
+ queryPhraseTokens: queryPhraseTokens,
374
567
  importChunkWeight: set.config.importChunkWeight, importSourceWeight: set.config.importSourceWeight,
375
568
  includeContent: set.includeContent, config: set.config, nowMs: Date.now(),
376
569
  }));
@@ -466,7 +659,7 @@ class FAM_Retrieval_ControlService {
466
659
  const lexicalScore = set.config.keywordWeight > 0
467
660
  ? fam_lexical_match_util_1.FAM_LexicalMatch_Util.score(set.queryTerms, {
468
661
  content: set.entry.content, headingPath: set.entry.headingPath, sourceFilePath: set.entry.source?.path,
469
- })
662
+ }, set.queryPhraseTokens)
470
663
  : 0;
471
664
  const keywordBoost = 1 + set.config.keywordWeight * lexicalScore;
472
665
  // PROJEKT-SZINTU szorzo a `.famconfig.json`-bol — SZANDEKOSAN a temperalason KIVUL. A `weight` mezo az
@@ -727,6 +920,24 @@ class FAM_Retrieval_ControlService {
727
920
  const memoryFloorWeight = await this.resolveNumber(config, 'memory.floorWeight', ctx, 0.05);
728
921
  const memoryMaxFrequencyBoost = await this.resolveNumber(config, 'memory.maxFrequencyBoost', ctx, 2.5);
729
922
  const memoryActivationWeight = await this.resolveNumber(config, 'memory.activationWeight', ctx, 0.35);
923
+ // KERESZT-TÁR „mellesleg"-javaslat (2026-09-01) — a `hits`-hez SOHA nem nyúl, csak külön mezőt ad.
924
+ // SZO SZERINTI IDEZET-MENTES (2026-09-02) — recall-javitas, meret-kapuval.
925
+ const literalRescueValue = (await config.resolve('read.literalRescueEnabled', ctx)).value;
926
+ const literalRescueEnabled = literalRescueValue !== false;
927
+ const literalRescueMaxDocs = await this.resolveNumber(config, 'read.literalRescueMaxDocs', ctx, 10000);
928
+ const literalRescueLimit = await this.resolveNumber(config, 'read.literalRescueLimit', ctx, 10);
929
+ const crossTableNoteValue = (await config.resolve('read.crossTableNoteEnabled', ctx)).value;
930
+ const crossTableNoteEnabled = crossTableNoteValue !== false;
931
+ const crossTableNoteMinDelta = await this.resolveNumber(config, 'read.crossTableNoteMinDelta', ctx, 0.025);
932
+ const crossTableNoteMinScore = await this.resolveNumber(config, 'read.crossTableNoteMinScore', ctx, 0.45);
933
+ const crossTableNoteTopK = await this.resolveNumber(config, 'read.crossTableNoteTopK', ctx, 3);
934
+ const probeTablesRaw = (await config.resolve('read.crossTableNoteProbeTables', ctx)).value;
935
+ // Csak az ERVENYES szonda-tarak mennek at (memory/knowledge) — elgepelt config nem tud mas tarat szondazni.
936
+ const crossTableNoteProbeTables = String(typeof probeTablesRaw === 'string' ? probeTablesRaw : 'memory')
937
+ .split(',')
938
+ .map((item) => item.trim())
939
+ .filter((item) => item === fam_table_type_enum_1.FAM_Table.memory || item === fam_table_type_enum_1.FAM_Table.knowledge)
940
+ .map((item) => item);
730
941
  const memoryReactivateValue = (await config.resolve('memory.reactivateOnRead', ctx)).value;
731
942
  const memoryReactivateOnRead = memoryReactivateValue !== false;
732
943
  const memoryColdSearchValue = (await config.resolve('memory.coldSearchEnabled', ctx)).value;
@@ -736,6 +947,14 @@ class FAM_Retrieval_ControlService {
736
947
  return {
737
948
  topK: Math.min(topKDefault, exports.MAX_PAGE_SIZE),
738
949
  minScore: minScore,
950
+ literalRescueEnabled: literalRescueEnabled,
951
+ literalRescueMaxDocs: literalRescueMaxDocs,
952
+ literalRescueLimit: literalRescueLimit,
953
+ crossTableNoteEnabled: crossTableNoteEnabled,
954
+ crossTableNoteMinDelta: crossTableNoteMinDelta,
955
+ crossTableNoteMinScore: crossTableNoteMinScore,
956
+ crossTableNoteTopK: crossTableNoteTopK,
957
+ crossTableNoteProbeTables: crossTableNoteProbeTables.length ? crossTableNoteProbeTables : [fam_table_type_enum_1.FAM_Table.memory],
739
958
  relevanceFloor: relevanceFloor,
740
959
  importChunkWeight: importChunkWeight,
741
960
  importSourceWeight: importSourceWeight,
@@ -10,11 +10,16 @@
10
10
  * opcionális relevance-filter = BACKLOG (NEM épül).
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.FAM_RetrievalSuggestions_Util = exports.FAM_RetrievalCandidate_DataService = exports.FAM_RuleScopeContext_Util = exports.FAM_RulePropagation_Util = exports.FAM_RuleInjection_Util = exports.FAM_RuleInjection_ControlService = exports.FAM_GlobalRule_ControlService = exports.MAX_PAGE_SIZE = exports.FAM_Retrieval_ControlService = void 0;
13
+ exports.FAM_RetrievalSuggestions_Util = exports.FAM_RetrievalCandidate_DataService = exports.FAM_RuleScopeContext_Util = exports.FAM_RulePropagation_Util = exports.FAM_RuleInjection_Util = exports.FAM_RuleInjection_ControlService = exports.FAM_GlobalRule_ControlService = exports.FAM_MemoryRetention_Util = exports.FAM_MemoryRetention_ControlService = exports.MAX_PAGE_SIZE = exports.FAM_Retrieval_ControlService = void 0;
14
14
  // SP-5.1/5.2/5.3 — a read-pipeline orchestrátor
15
15
  var fam_retrieval_control_service_1 = require("./_services/fam-retrieval.control-service");
16
16
  Object.defineProperty(exports, "FAM_Retrieval_ControlService", { enumerable: true, get: function () { return fam_retrieval_control_service_1.FAM_Retrieval_ControlService; } });
17
17
  Object.defineProperty(exports, "MAX_PAGE_SIZE", { enumerable: true, get: function () { return fam_retrieval_control_service_1.MAX_PAGE_SIZE; } });
18
+ // Memoria-retencio (2026-09-02): az alvo tormelek heti nyugdijazasa — a dontesi logika a pure utilban.
19
+ var fam_memory_retention_control_service_1 = require("./_services/fam-memory-retention.control-service");
20
+ Object.defineProperty(exports, "FAM_MemoryRetention_ControlService", { enumerable: true, get: function () { return fam_memory_retention_control_service_1.FAM_MemoryRetention_ControlService; } });
21
+ var fam_memory_retention_util_1 = require("./_collections/fam-memory-retention.util");
22
+ Object.defineProperty(exports, "FAM_MemoryRetention_Util", { enumerable: true, get: function () { return fam_memory_retention_util_1.FAM_MemoryRetention_Util; } });
18
23
  var fam_global_rule_control_service_1 = require("./_services/fam-global-rule.control-service");
19
24
  Object.defineProperty(exports, "FAM_GlobalRule_ControlService", { enumerable: true, get: function () { return fam_global_rule_control_service_1.FAM_GlobalRule_ControlService; } });
20
25
  // Rules-Management MP-2 — always-apply rule-injektálás (SessionStart inject + read-reflex) szerver-oldali assembly