@futdevpro/fdp-agent-memory 1.1.177 → 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 (22) hide show
  1. package/build/package.json +1 -1
  2. package/build/src/_cli/_commands/retire-temporary.command.js +77 -0
  3. package/build/src/_cli/register-commands.js +2 -0
  4. package/build/src/_collections/config-catalog.const.js +44 -0
  5. package/build/src/_models/data-models/fam-entry.data-model.js +6 -0
  6. package/build/src/_models/data-models/fam-memory.data-model.js +3 -0
  7. package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +32 -0
  8. package/build/src/_modules/ingest/_services/fam-scan-scheduler.control-service.js +26 -0
  9. package/build/src/_modules/mcp/_collections/fam-core-tools.const.js +5 -0
  10. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +1 -0
  11. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +29 -7
  12. package/build/src/_modules/retrieval/_collections/fam-cross-table-note.util.js +10 -3
  13. package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +75 -2
  14. package/build/src/_modules/retrieval/_collections/fam-literal-phrase.util.js +74 -0
  15. package/build/src/_modules/retrieval/_collections/fam-memory-retention.util.js +70 -0
  16. package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +8 -1
  17. package/build/src/_modules/retrieval/_services/fam-memory-retention.control-service.js +118 -0
  18. package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +33 -0
  19. package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +127 -7
  20. package/build/src/_modules/retrieval/index.js +6 -1
  21. package/build/src/_routes/server/api/api.controller.js +46 -15
  22. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futdevpro/fdp-agent-memory",
3
- "version": "1.1.177",
3
+ "version": "1.1.194",
4
4
  "description": "Local-first, vector-backed multi-table agent memory exposed as an MCP server (read/write/capabilities). Public, FDP-Templates-free, no auth.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_RetireTemporary_Util = void 0;
4
+ const commander_1 = require("commander");
5
+ const fam_output_util_1 = require("../_collections/fam-output.util");
6
+ const fam_client_service_1 = require("../_services/fam-client.service");
7
+ const fam_cli_const_1 = require("../_collections/fam-cli.const");
8
+ /**
9
+ * `fam retire-temporary [--apply]` — az alvó beszélgetés-törmelék nyugdíjazása a `memory` táron.
10
+ *
11
+ * A szabály (v2, owner: „javítsd, amit kell" — 2026-09-02): törmelék-osztály (`user_prompt` /
12
+ * `temporary_context`) ÉS 30+ napos → `superseded + retiredAt`, a felidézés-számtól FÜGGETLENÜL (mérve:
13
+ * a recallCount ezen az osztályon öngerjesztő rangsor-kitettséget mért — a csúcs-„felidézett" egy
14
+ * task-notification XML-blokk volt recall=281-gyel). **NEM törlés** — a tartalom és a vektor marad, a
15
+ * rangsor-büntetés süllyeszti, a cold-search megtalálja. **Alapértelmezés a DRY-RUN** — `--apply` kell.
16
+ *
17
+ * A heti scan-scheduler automatikusan is futtatja (`memory.retireTemporaryEnabled`); ez a parancs a
18
+ * kézi/azonnali út + a dry-run előnézet.
19
+ */
20
+ class FAM_RetireTemporary_Util {
21
+ /** A `retire-temporary` parancs-deszkriptor. */
22
+ static command() {
23
+ const command = new commander_1.Command('retire-temporary');
24
+ command
25
+ .description('Az alvó törmelék (user_prompt/temporary_context) nyugdíjazása (DRY-RUN az alapértelmezés)')
26
+ .option('--apply', 'a nyugdíjazás TÉNYLEGES végrehajtása (enélkül csak mér, nem ír)')
27
+ .option('--after-days <n>', 'a korhatár felülírása erre a futásra (default: config, 30 nap)')
28
+ .action(async () => {
29
+ fam_output_util_1.FAM_Output_Util.exit(await FAM_RetireTemporary_Util.run(command));
30
+ });
31
+ return command;
32
+ }
33
+ /** A `retire-temporary` futtatása a REST `POST /api/memory/retire-temporary`-n. */
34
+ static async run(command) {
35
+ const globals = command.optsWithGlobals();
36
+ const local = command.opts();
37
+ const client = new fam_client_service_1.FAM_CliClient_Service(globals.serverUrl);
38
+ const afterDays = local.afterDays !== undefined ? Number(local.afterDays) : undefined;
39
+ const result = await client.post('/api/memory/retire-temporary', {
40
+ // A `--apply` HIÁNYA a biztonságos irány: dryRun marad. Elgépelt kapcsoló sosem nyugdíjaz.
41
+ dryRun: local.apply !== true,
42
+ retireAfterDays: Number.isFinite(afterDays) ? afterDays : undefined,
43
+ });
44
+ if (!result.ok) {
45
+ return fam_output_util_1.FAM_Output_Util.failure({
46
+ options: globals,
47
+ command: 'retire-temporary',
48
+ error: result.error ?? { errorCode: 'FAM-CLI-RETIRE-001', message: 'Ismeretlen retenció-hiba.' },
49
+ exitCode: result.unreachable ? fam_cli_const_1.FAM_CliExitCode.serverUnreachable : fam_cli_const_1.FAM_CliExitCode.operationError,
50
+ });
51
+ }
52
+ return fam_output_util_1.FAM_Output_Util.success({
53
+ options: globals,
54
+ command: 'retire-temporary',
55
+ data: result.data,
56
+ humanFormatter: (data) => FAM_RetireTemporary_Util.format(data),
57
+ });
58
+ }
59
+ /** Ember-olvasható összegzés — a MEGTARTÁS okai is látszanak (a védelem is jelentendő tény). */
60
+ static format(report) {
61
+ const lines = [];
62
+ lines.push(report.dryRun
63
+ ? `Memória-retenció — DRY-RUN (semmi nem változott; végrehajtás: --apply; korhatár ${report.retireAfterDays} nap):`
64
+ : `Memória-retenció — VÉGREHAJTVA (korhatár ${report.retireAfterDays} nap):`);
65
+ lines.push(` vizsgált törmelék-jelölt: ${report.scanned}`);
66
+ lines.push(` nyugdíjazva: ${report.retired} (superseded+retiredAt — NEM törlés, hidegen kereshető)`);
67
+ lines.push(` MEGTARTVA — még fiatal: ${report.keptTooYoung}`);
68
+ if (report.alreadySuperseded) {
69
+ lines.push(` már korábban nyugdíjas: ${report.alreadySuperseded}`);
70
+ }
71
+ if (!report.scanned) {
72
+ lines.push(' ✓ nincs törmelék-jelölt a memory táron.');
73
+ }
74
+ return lines.join('\n');
75
+ }
76
+ }
77
+ exports.FAM_RetireTemporary_Util = FAM_RetireTemporary_Util;
@@ -9,6 +9,7 @@ const config_command_1 = require("./_commands/config.command");
9
9
  const stats_command_1 = require("./_commands/stats.command");
10
10
  const find_duplicates_command_1 = require("./_commands/find-duplicates.command");
11
11
  const prune_duplicates_command_1 = require("./_commands/prune-duplicates.command");
12
+ const retire_temporary_command_1 = require("./_commands/retire-temporary.command");
12
13
  const errors_command_1 = require("./_commands/errors.command");
13
14
  const init_command_1 = require("./_commands/init.command");
14
15
  const doctor_command_1 = require("./_commands/doctor.command");
@@ -58,6 +59,7 @@ function registerFAMCommands(program, version) {
58
59
  stats_command_1.FAM_Stats_Util.command(),
59
60
  find_duplicates_command_1.FAM_FindDuplicates_Util.command(),
60
61
  prune_duplicates_command_1.FAM_PruneDuplicates_Util.command(),
62
+ retire_temporary_command_1.FAM_RetireTemporary_Util.command(),
61
63
  errors_command_1.FAM_Errors_Util.command(),
62
64
  // 7 delegáló alias (README copy-paste-elhetőség, nincs külön logika; dsgn-010 §10).
63
65
  doctor_command_1.FAM_Doctor_Util.aliasCommand('validate-env', ['env']),
@@ -191,6 +191,27 @@ exports.CONFIG_CATALOG = {
191
191
  // eros ("miert tort el", "lattuk mar ezt"), de az agent jellemzoen `codebase`/`documents`-ben keres, mert
192
192
  // nem tudja, hogy ott van. Merve 15 realisztikus kerdesen: 13-nal az ELSODLEGES tar volt jobb -> a talalat
193
193
  // NEM kerulhet a listaba (13 esetben rontanank, hogy 2-ben javitsunk), csak kulon, egyetlen mellek-mezobe.
194
+ // SZO SZERINTI IDEZET-MENTES (2026-09-02, black-box lelet). A bemasolt hibauzenet/idezet chunkja BE SEM
195
+ // KERUL a vektor-jeloltek koze (merve: 60 talalatbol 0 tartalmazta a keresett stringet) — ez recall-hiba.
196
+ // A mentes a FEAT-003 kod-expanzio mintajara determinisztikusan behozza a szo szerint egyezo entry-ket.
197
+ 'read.literalRescueEnabled': {
198
+ type: 'boolean', default: true, levels: ALL_LEVELS,
199
+ description: 'Szo szerinti idezet-mentes: ha a query IDEZET-SZERU (>=5 token, van benne disztinktiv '
200
+ + 'token), a pontosan egyezo entry-k a vektor-uton KIVUL is bekerulnek. A talalati lista tobbi '
201
+ + 'eleme valtozatlan marad.',
202
+ },
203
+ 'read.literalRescueMaxDocs': {
204
+ type: 'number', default: 10000, min: 0, max: 1000000, integer: true, levels: ALL_LEVELS,
205
+ description: 'MERET-KAPU: a mentes csak ennyi dokumentumnal kisebb taron fut (a content-substring NEM '
206
+ + 'indexelheto -> collection-scan). MERVE 2026-09-02: rules 296 ms · coding_patterns 504 ms · '
207
+ + 'knowledge 2,1 s · memory 4,9 s · documents 13,1 s · codebase 45,2 s. A default 10000 a rules-t '
208
+ + 'es a coding_patterns-t engedi be (~0,8 s legrosszabb eset), a tobbit KIHAGYJA — a mentes soha '
209
+ + 'nem tehet egy olvasast masodperces muvellette.',
210
+ },
211
+ 'read.literalRescueLimit': {
212
+ type: 'number', default: 10, min: 1, max: 100, integer: true, levels: ALL_LEVELS,
213
+ description: 'Hany mentett entry kerulhet be tarankent (a talalati lista nem arasztodhat el).',
214
+ },
194
215
  'read.crossTableNoteEnabled': {
195
216
  type: 'boolean', default: true, levels: ALL_LEVELS,
196
217
  description: 'Kereszt-tar "mellesleg"-javaslat: ha a memory/knowledge taron ERDEMBEN erosebb talalat van, '
@@ -213,6 +234,14 @@ exports.CONFIG_CATALOG = {
213
234
  description: 'Abszolut also korlat a jelolt sajat score-jara: gyenge talalatot akkor sem ajanlunk, '
214
235
  + 'ha az elsodleges tar meg gyengebb (kulonben a "nincs talalat" eset minden alkalommal zajt szulne).',
215
236
  },
237
+ 'read.crossTableNoteProbeTables': {
238
+ type: 'string', default: 'memory', levels: ALL_LEVELS,
239
+ description: 'A szondazott tarak (vesszo-szeparalt; ervenyes: memory, knowledge). Default CSAK memory — '
240
+ + 'MERVE (2026-09-02): a 44 esetes benchmark 21 szonda-jeloltjebol MIND a memory tarbol jott, a '
241
+ + 'knowledge 0-t adott (a talalatai a tormelek-szures utan mind kiestek), mikozben a szondaja a '
242
+ + 'wall-clock koltseg ~felet vitte (a ket pool-koszinusz a Node-szalon sorosodik). A knowledge '
243
+ + 'visszakapcsolhato: "memory,knowledge".',
244
+ },
216
245
  'read.crossTableNoteTopK': {
217
246
  type: 'number', default: 3, min: 1, max: 10, integer: true, levels: ALL_LEVELS,
218
247
  description: 'A szonda topK-ja taranként. Kicsi: a szonda csak azt donti el, VAN-E erdemben erosebb — '
@@ -360,6 +389,21 @@ exports.CONFIG_CATALOG = {
360
389
  description: 'A cold-search dormant-scan FELSŐ KORLÁTJA (biztonsági memóriakorlát, dsgn-013 §5.3): a Mongo-koszinusz '
361
390
  + 'legfeljebb ennyi dormant entry vektorát tölti be egy keresésnél. Default 5000.',
362
391
  },
392
+ // RETENCIO (2026-09-02): az alvo beszelgetes-tormelek (user_prompt / temporary_context) heti
393
+ // nyugdijazasa. Merve: 8 717 ilyen bejegyzes (a memory tar 20,2%-a) egy egyszeri importbol, 98,2%-uk
394
+ // SOHA nem lett felidezve — megis teljes sullyal versenyeztek a gondozott jegyzetekkel. A nyugdij
395
+ // NEM torles: superseded+retiredAt, a supersededPenalty sullyeszti, a cold-search megtalalja.
396
+ 'memory.retireTemporaryEnabled': {
397
+ type: 'boolean', default: true, levels: ALL_LEVELS,
398
+ description: 'A heti automatikus retencio kapcsoloja: a scan-scheduler a heti re-scan UTAN nyugdijazza '
399
+ + 'az alvo tormelek-bejegyzeseket. A szabaly: tormelek-osztaly ES sosem felidezett ES 30+ napos. '
400
+ + 'A valaha felidezett bejegyzes OROKRE aktiv marad ("amit tobbet hasznaltunk, az tovabb el").',
401
+ },
402
+ 'memory.retireTemporaryAfterDays': {
403
+ type: 'number', default: 30, min: 1, max: 3650, integer: true, levels: ALL_LEVELS,
404
+ description: 'Hany nap utan nyugdijazhato a SOSEM felidezett tormelek-bejegyzes. A friss kontextusnak '
405
+ + 'igy van eselye felidezodni, mielott a retencio eleri.',
406
+ },
363
407
  'memory.supersededPenalty': {
364
408
  type: 'number', default: 0.25, min: 0, max: 1, levels: ALL_LEVELS,
365
409
  description: 'A LEVÁLTOTT (superseded) memória ranking-PENALTYje (dsgn-013 §6): egy additív memory-fájl régi, már '
@@ -86,6 +86,12 @@ class FAM_Entry extends fsm_dynamo_1.DyFM_Metadata {
86
86
  * (`memory.supersededPenalty`), így a friss verzió fölébe kerül. A content-addressed additív delta jelöli.
87
87
  */
88
88
  superseded;
89
+ /**
90
+ * MIKOR vonult nyugdíjba (retenció, 2026-09-02): a `superseded`-re váltás időbélyege (epoch ms), ha azt
91
+ * NEM a tartalom-leváltás, hanem a retenció vagy a memory-táras write-delete adta. Auditálhatóság: ebből
92
+ * látszik, hogy a bejegyzés MIÉRT hideg — leváltották vagy nyugdíjazták. Hiánya = tartalom-leváltás.
93
+ */
94
+ retiredAt;
89
95
  // --- forrás / pozíció (scan/import esetén) ---
90
96
  /** Provenance (dsgn-001 §6). */
91
97
  source;
@@ -56,5 +56,8 @@ exports.famMemory_dataParams = new fsm_dynamo_1.DyFM_DataModel_Params({
56
56
  recallCount: { type: 'number', default: 0 },
57
57
  dormant: { type: 'boolean', index: true },
58
58
  superseded: { type: 'boolean', index: true },
59
+ // Retenció (2026-09-02): a nyugdíjazás időbélyege — ebből látszik, hogy a bejegyzés MIÉRT hideg
60
+ // (nyugdíjazták vs a tartalmát leváltották). Az utóbbi esetben hiányzik.
61
+ retiredAt: { type: 'number' },
59
62
  },
60
63
  });
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_EmbeddingBootstrap_ControlService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
4
6
  const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
7
  const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
6
8
  const fam_dup_audit_scheduler_control_service_1 = require("./fam-dup-audit-scheduler.control-service");
@@ -10,6 +12,8 @@ const fam_scope_maintenance_control_service_1 = require("../../scope-reference/_
10
12
  const fam_scan_job_control_service_1 = require("../../ingest/_services/fam-scan-job.control-service");
11
13
  const fam_scan_scheduler_control_service_1 = require("../../ingest/_services/fam-scan-scheduler.control-service");
12
14
  const fam_memory_dormancy_control_service_1 = require("../../retrieval/_services/fam-memory-dormancy.control-service");
15
+ const fam_store_registry_const_1 = require("../_collections/fam-store-registry.const");
16
+ const fam_entry_data_service_1 = require("./fam-entry.data-service");
13
17
  /**
14
18
  * `FAM_EmbeddingBootstrap_ControlService` (SP-2.2/2.4) — az embedding + vektor-réteg **boot-hook**-ja.
15
19
  * Az `App.postProcess()` (a DB-connect + setup UTÁNI hook) hívja. Két lépés:
@@ -65,6 +69,11 @@ class FAM_EmbeddingBootstrap_ControlService {
65
69
  catch (error) {
66
70
  fsm_dynamo_1.DyFM_Log.warn(`[FAM bootstrap] dup-őrjárat scheduler telepítése sikertelen: ${error?.message}`);
67
71
  }
72
+ // TELJESÍTMÉNY-INDEXEK (2026-09-02, mért incidens): a `/stats` (és minden `__lastModified`-sort /
73
+ // `_deleted`-szűrt / scope-count lekérdezés) COLLSCAN volt — meleg Mongo-cache mellett észrevétlen,
74
+ // kihűlt cache + telített lemezen viszont 20–50 s/tár, a UI landing PERCEKIG lógott. A createIndex
75
+ // idempotens (létező indexre no-op); háttérben fut, a boot nem várja be (fire-and-forget, WARN-nal).
76
+ void this.ensurePerformanceIndexes();
68
77
  // A readiness-coordinator hidratálás-bevárás time-outja a `read.hydrationWaitMs` config-ból (a hidratálás
69
78
  // ELŐTT, hogy a `beginBoot` után érkező read-ek már a helyes időkorláttal várjanak). Best-effort (default 180s).
70
79
  try {
@@ -122,5 +131,28 @@ class FAM_EmbeddingBootstrap_ControlService {
122
131
  fsm_dynamo_1.DyFM_Log.warn(`[FAM bootstrap] memory-dormancy sweep sikertelen: ${error?.message}`);
123
132
  }
124
133
  }
134
+ /**
135
+ * A mért lassú lekérdezés-alakok indexei MINDEN entry-táron (2026-09-02): `__lastModified` (a stats
136
+ * "utolsó módosítás" sort-ja), `_deleted` (az aktív-szűrős countok), `scopePath.scopeId` (a scope-countok
137
+ * és a delta-compare scope-szűrője). Idempotens; a hiba WARN — az index-hiány lassulás, nem leállás.
138
+ */
139
+ async ensurePerformanceIndexes() {
140
+ for (const registryEntry of fam_store_registry_const_1.FAM_STORE_REGISTRY) {
141
+ try {
142
+ new fam_entry_data_service_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: 'boot-indexes' });
143
+ const collection = mongoose_1.default.models[registryEntry.dataParams.dataName]?.collection;
144
+ if (!collection) {
145
+ continue;
146
+ }
147
+ await collection.createIndex({ __lastModified: -1 });
148
+ await collection.createIndex({ _deleted: 1 });
149
+ await collection.createIndex({ 'scopePath.scopeId': 1 });
150
+ }
151
+ catch (error) {
152
+ fsm_dynamo_1.DyFM_Log.warn(`[FAM bootstrap] teljesítmény-index kiépítés sikertelen (${registryEntry.table}): `
153
+ + `${error?.message}`);
154
+ }
155
+ }
156
+ }
125
157
  }
126
158
  exports.FAM_EmbeddingBootstrap_ControlService = FAM_EmbeddingBootstrap_ControlService;
@@ -4,6 +4,7 @@ exports.FAM_ScanScheduler_ControlService = void 0;
4
4
  const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
5
  const fam_config_level_type_enum_1 = require("../../../_enums/fam-config-level.type-enum");
6
6
  const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
7
+ const retrieval_1 = require("../../retrieval");
7
8
  const fam_scan_job_control_service_1 = require("./fam-scan-job.control-service");
8
9
  const fam_scan_job_data_service_1 = require("./fam-scan-job.data-service");
9
10
  const embedding_1 = require("../../embedding");
@@ -120,6 +121,10 @@ class FAM_ScanScheduler_ControlService {
120
121
  + `(slot: ${FAM_ScanScheduler_ControlService.DAY_NAMES[day] ?? day} ${hour}:00). Indítás…`);
121
122
  await this.markRan(config, nowMs);
122
123
  await this.triggerRescan();
124
+ // RETENCIÓ (2026-09-02): a heti ütemben az alvó törmelék nyugdíjazása is fut — a lényege az
125
+ // automatizmus (különben a törmelék sosem vonul nyugdíjba). A scan-tól FÜGGETLEN munka, nem
126
+ // várja meg (a scan async, waitMs:0); best-effort: a hibája nem buktatja a ticket.
127
+ await this.runWeeklyRetention();
123
128
  }
124
129
  catch (error) {
125
130
  fsm_dynamo_1.DyFM_Log.warn(`[FAM scanScheduler] heti re-scan tick sikertelen (best-effort): ${error?.message}`);
@@ -128,6 +133,27 @@ class FAM_ScanScheduler_ControlService {
128
133
  this.ticking = false;
129
134
  }
130
135
  }
136
+ /**
137
+ * A heti retenció: az alvó törmelék (`user_prompt`/`temporary_context`, sosem felidézett, 30+ napos)
138
+ * nyugdíjazása. Kapu: `memory.retireTemporaryEnabled` (default true). A szabály + indoklás:
139
+ * `retrieval/_collections/fam-memory-retention.util.ts`. Best-effort — a hiba WARN, nem tick-bukás.
140
+ */
141
+ async runWeeklyRetention() {
142
+ try {
143
+ const retention = retrieval_1.FAM_MemoryRetention_ControlService.getInstance();
144
+ if (!(await retention.isAutoEnabled())) {
145
+ fsm_dynamo_1.DyFM_Log.log('[FAM scanScheduler] heti retenció KIKAPCSOLVA (`memory.retireTemporaryEnabled=false`) — kihagyva.');
146
+ return;
147
+ }
148
+ const report = await retention.retire({ dryRun: false });
149
+ fsm_dynamo_1.DyFM_Log.log(`[FAM scanScheduler] heti retenció kész: ${report.retired} nyugdíjazva `
150
+ + `(${report.scanned} jelöltből; ${report.keptTooYoung} még fiatal, `
151
+ + `${report.alreadySuperseded} már nyugdíjas).`);
152
+ }
153
+ catch (error) {
154
+ fsm_dynamo_1.DyFM_Log.warn(`[FAM scanScheduler] heti retenció sikertelen (best-effort): ${error?.message}`);
155
+ }
156
+ }
131
157
  /** A TELJES re-scan indítása: `scan.weeklyRescanRoot` (ha van) VAGY a legutóbbi durable scan-job cél-specjei. */
132
158
  async triggerRescan() {
133
159
  const origin = { agent: 'fam-scheduler', label: 'weekly-rescan' };
@@ -66,6 +66,11 @@ const READ_INPUT_SCHEMA = {
66
66
  description: 'Feltárás-mélység: `deep` → mély feltárás (sokkal több chunk; a `read.deepTopK` '
67
67
  + 'az alap topK helyett). Default `normal`. Az explicit `topK` mindig felülír.',
68
68
  },
69
+ skipReactivation: {
70
+ type: 'boolean',
71
+ description: 'DIAGNOSZTIKA: a visszaadott memory-hitek NE kapjanak felidézés-megerősítést '
72
+ + '(recallCount++). Mérő-/kalibrációs forgalomhoz — a mérés nem terhelheti a mért rendszert.',
73
+ },
69
74
  includeCrossTableProbe: {
70
75
  type: 'boolean',
71
76
  description: 'DIAGNOSZTIKA (nem termék-funkció): a kereszt-tár szonda nyers döntési adata '
@@ -320,6 +320,7 @@ class FAM_ReadTool_Service {
320
320
  excludeIds: query.excludeIds,
321
321
  depth: query.depth,
322
322
  includeCrossTableProbe: query.includeCrossTableProbe,
323
+ skipReactivation: query.skipReactivation,
323
324
  // Per-query tartalom-kapcsolo atvezetese (2026-08-14 lelet: eddig itt VESZETT EL).
324
325
  includeContent: query.includeContent,
325
326
  };
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_WriteTool_Service = void 0;
4
4
  const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
+ const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
5
6
  const error_codes_const_1 = require("../../../_collections/error-codes.const");
6
7
  const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
7
8
  const fam_entry_data_model_1 = require("../../../_models/data-models/fam-entry.data-model");
@@ -333,19 +334,40 @@ class FAM_WriteTool_Service {
333
334
  // FAM-REV-051 bulk: `sourceFilePath`/`absolutePath` → egy fájl ÖSSZES chunkja; `id`/`query` → egyetlen.
334
335
  const targets = await this.resolveDeleteTargets(input, dataService, 'write-delete');
335
336
  const deletedIds = [];
337
+ // RETIRE-NOT-DELETE a `memory` táron (2026-09-02, owner-elv: „nem baj, ha fennmarad olyan memória,
338
+ // amit a Claude már elfelejtett — sőt!"). A memória-törlés NYUGDÍJAZÁS: `superseded + retiredAt`,
339
+ // a tartalom és a vektor marad (a penalty süllyeszti, a cold-search megtalálja). Más táron a
340
+ // viselkedés VÁLTOZATLAN — ott a fájl az igazság, a scan a törölt chunkot úgyis újra behozná.
341
+ // Valódi hard-delete-re a scope-maintenance reset az admin-út.
342
+ const retireInsteadOfDelete = input.table === fam_table_type_enum_1.FAM_Table.memory;
343
+ const now = Date.now();
336
344
  for (const entry of targets) {
337
345
  if (!entry._id) {
338
346
  continue;
339
347
  }
340
- // absolute=false → soft-delete (Dynamo-konvenció: `_deleted` marker, archív collection).
341
- await dataService.deleteData(entry._id, false);
342
- // Pool-coherence (FAM-REV-046): a törölt vektort a hívó-process in-memory LVS-pool-jából is kivesszük.
343
- embedding_1.FAM_VectorSearch_ControlService.getInstance().removeVector(input.table, entry._id);
348
+ if (retireInsteadOfDelete) {
349
+ await dataService.updateData({
350
+ filterBy: { _id: entry._id },
351
+ update: { $set: { superseded: true, retiredAt: now } },
352
+ });
353
+ // A vektor a poolban MARAD — a superseded-szemantika része (a rangsor-büntetés süllyeszti).
354
+ }
355
+ else {
356
+ // absolute=false → soft-delete (Dynamo-konvenció: `_deleted` marker, archív collection).
357
+ await dataService.deleteData(entry._id, false);
358
+ // Pool-coherence (FAM-REV-046): a törölt vektort a hívó-process in-memory LVS-pool-jából is kivesszük.
359
+ embedding_1.FAM_VectorSearch_ControlService.getInstance().removeVector(input.table, entry._id);
360
+ }
344
361
  deletedIds.push(entry._id);
345
362
  }
346
- const warnings = deletedIds.length > 1
347
- ? [`Bulk file-delete: ${deletedIds.length} chunk soft-delete-elve (cél: ${input.target?.by}='${input.target?.value}').`]
348
- : [];
363
+ const warnings = [];
364
+ if (retireInsteadOfDelete && deletedIds.length) {
365
+ warnings.push(`A memory táron a törlés NYUGDÍJAZÁS: ${deletedIds.length} bejegyzés superseded+retiredAt `
366
+ + 'jelölést kapott — hidegen kereshető marad, nem törlődött (owner-elv: a felejtett memória is érték).');
367
+ }
368
+ else if (deletedIds.length > 1) {
369
+ warnings.push(`Bulk file-delete: ${deletedIds.length} chunk soft-delete-elve (cél: ${input.target?.by}='${input.target?.value}').`);
370
+ }
349
371
  return this.baseOutput({
350
372
  operation: input.operation, table: input.table,
351
373
  scopePath: (targets[0]?.scopePath ?? []),
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_CrossTableNote_Util = void 0;
4
+ const fam_memory_retention_util_1 = require("./fam-memory-retention.util");
4
5
  /**
5
6
  * NEM-TUDÁS jelöltek: nyers beszélgetés-törmelék, amivel SOHA nem szabad „mellesleg"-et adni.
6
7
  *
@@ -14,8 +15,11 @@ exports.FAM_CrossTableNote_Util = void 0;
14
15
  * összefoglalója. A benchmarkban pont a döntési zónában ült (+0,022), és semmilyen kérdésre nem válasz.
15
16
  * (A scan-summary zaja külön is mért, ismert probléma — `reference_fam_scan_summary_weight_cap`.)
16
17
  */
17
- const NON_KNOWLEDGE_KINDS = ['user_prompt', 'fs-summary'];
18
- const NON_KNOWLEDGE_TAGS = ['temporary_context', 'scan-summary'];
18
+ // A MEMORIA-tormelek definicioja a KOZOS utilbol jon (fam-memory-retention.util) — egy definicio,
19
+ // harom felhasznalas (retencio + reaktivacio-szuro + kereszt-tar szonda). Itt CSAK a knowledge-tar
20
+ // sajat zaja (fs-summary/scan-summary) egeszul ki.
21
+ const NON_KNOWLEDGE_KINDS = ['fs-summary'];
22
+ const NON_KNOWLEDGE_TAGS = ['scan-summary'];
19
23
  class FAM_CrossTableNote_Util {
20
24
  /**
21
25
  * A döntés. `undefined` = **csend**: nincs mező a válaszban. Szándékosan NEM adunk „nem találtam
@@ -56,8 +60,11 @@ class FAM_CrossTableNote_Util {
56
60
  }),
57
61
  };
58
62
  }
59
- /** Beszélgetés-törmelék-e a jelölt (nyers user-prompt / átmeneti kontextus). Lásd a fenti mérést. */
63
+ /** Beszélgetés-törmelék / gépi zaj-e a jelölt. A memória-törmelék részt a közös util dönti el. */
60
64
  static isNonKnowledge(candidate) {
65
+ if (fam_memory_retention_util_1.FAM_MemoryRetention_Util.isDebris({ kind: candidate.kind, tags: candidate.tags })) {
66
+ return true;
67
+ }
61
68
  if (candidate.kind && NON_KNOWLEDGE_KINDS.includes(candidate.kind)) {
62
69
  return true;
63
70
  }
@@ -31,6 +31,25 @@ class FAM_LexicalMatch_Util {
31
31
  ];
32
32
  /** A tő MINIMÁLIS hossza tövezés után (a túl-tövezés hamis egyezéseket hozna). */
33
33
  static MIN_STEM_LEN = 4;
34
+ /**
35
+ * SZÓ SZERINTI IDÉZET-EGYEZÉS (2026-09-02, black-box futás lelete). A term-átfedés egy HALMAZ-arány:
36
+ * nem tudja megkülönböztetni a „ugyanazok a szavak szétszórva" esetet a „pontosan ez a mondat van benne"
37
+ * esettől — pedig az utóbbi nagyságrenddel erősebb relevancia-jel.
38
+ *
39
+ * **Mért eset:** a `402 Payment Required - You must sign up for private packages` hibaüzenet szó szerinti
40
+ * bemásolása (a legtermészetesebb fejlesztői reflex) a `fdp-never-public-packages` szabályt a **#7**
41
+ * helyre tette 60 találatból — holott a szabály-fájl **literálisan tartalmazza ezt a mondatot**, és övé
42
+ * volt a mezőny LEGMAGASABB term-átfedése (0,325). A nyers koszinusza viszont csak 0,480 volt (egy
43
+ * kontextus nélküli hibaüzenet gyenge szemantikus jel), és a ×1,195-ös boost nem hozta fel.
44
+ *
45
+ * A phrase-bónusz ezt a hibaosztályt fedi — bemásolt hibaüzenet, idézett mondat, pontos azonosító —, és
46
+ * a PARAFRAZEÁLT kérdéseket NEM érinti: ott nincs hosszú, szó szerinti egyezés, tehát nincs bónusz sem.
47
+ */
48
+ static MIN_PHRASE_LEN = 3;
49
+ /** A phrase-bónusz maximuma (a teljes query-hossznyi szó szerinti egyezésre jár). */
50
+ static PHRASE_BONUS = 0.6;
51
+ /** A vizsgált query-tokenek plafonja — egy hosszú prompt ne csináljon O(n²) munkát hit-enként. */
52
+ static MAX_PHRASE_TOKENS = 24;
34
53
  /** Tokenizálás: kisbetűsít, nem-alfanumerikuson tör, stopword + rövid token kihagyva, DEDUP (Set). */
35
54
  static tokenize(text) {
36
55
  const terms = new Set();
@@ -62,12 +81,53 @@ class FAM_LexicalMatch_Util {
62
81
  }
63
82
  return undefined;
64
83
  }
84
+ /**
85
+ * NYERS, SORRENDHELYES token-sorozat (a phrase-egyezéshez). A `tokenize`-zal ellentétben **NEM** szűr
86
+ * stopwordöt és **NEM** dedupál: a szó szerinti idézetben a kötőszavak is a mondat részei — pont azok
87
+ * teszik idézetté. A rövid tokenek (`MIN_TOKEN_LEN` alatt) itt is kimaradnak, mert a normalizált
88
+ * szövegben sem szerepelnek.
89
+ */
90
+ static tokenizeSequence(text) {
91
+ if (!text) {
92
+ return [];
93
+ }
94
+ return text.toLowerCase()
95
+ .split(/[^a-z0-9áéíóöőúüű]+/)
96
+ .filter((token) => token.length >= FAM_LexicalMatch_Util.MIN_TOKEN_LEN);
97
+ }
98
+ /** Az entry kereshető szövegének normalizált alakja (kisbetűs, egy-szóközös) — a phrase-kereséshez. */
99
+ static normalizeForPhrase(text) {
100
+ if (!text) {
101
+ return '';
102
+ }
103
+ return ` ${text.toLowerCase().split(/[^a-z0-9áéíóöőúüű]+/).filter(Boolean).join(' ')} `;
104
+ }
105
+ /**
106
+ * A leghosszabb SZÓ SZERINTI, összefüggő query-részlet hossza, ami az entry szövegében is megvan
107
+ * (0, ha nincs `MIN_PHRASE_LEN`-nél hosszabb egyezés). A leghosszabb ablaktól indul és az ELSŐ
108
+ * egyezésnél visszatér — így a tipikus „nincs idézet" eset is korlátos munkával zárul.
109
+ */
110
+ static longestPhraseRun(queryTokens, normalizedEntryText) {
111
+ const tokens = queryTokens.slice(0, FAM_LexicalMatch_Util.MAX_PHRASE_TOKENS);
112
+ if (tokens.length < FAM_LexicalMatch_Util.MIN_PHRASE_LEN || !normalizedEntryText) {
113
+ return 0;
114
+ }
115
+ for (let length = tokens.length; length >= FAM_LexicalMatch_Util.MIN_PHRASE_LEN; length--) {
116
+ for (let start = 0; start + length <= tokens.length; start++) {
117
+ const phrase = ` ${tokens.slice(start, start + length).join(' ')} `;
118
+ if (normalizedEntryText.includes(phrase)) {
119
+ return length;
120
+ }
121
+ }
122
+ }
123
+ return 0;
124
+ }
65
125
  /**
66
126
  * Lexikai átfedés-score (0..1): a `queryTerms` hányad része van jelen az entry kereshető szövegében.
67
127
  * `content`-egyezés = base-átfedés; `headingPath`/`sourceFilePath`-egyezés = + `TITLE_PATH_BONUS` súly (1-re vágva).
68
128
  * Üres query-term-halmaz → 0 (nincs jel → nincs boost). PURE.
69
129
  */
70
- static score(queryTerms, entry) {
130
+ static score(queryTerms, entry, queryPhraseTokens) {
71
131
  if (!queryTerms.size) {
72
132
  return 0;
73
133
  }
@@ -94,7 +154,20 @@ class FAM_LexicalMatch_Util {
94
154
  const total = queryTerms.size;
95
155
  const base = baseMatched / total;
96
156
  const titleBonus = (titleMatched / total) * FAM_LexicalMatch_Util.TITLE_PATH_BONUS;
97
- return Math.min(1, base + titleBonus);
157
+ // SZÓ SZERINTI IDÉZET-BÓNUSZ: a query hány százaléka szerepel ÖSSZEFÜGGŐEN, szó szerint az entry
158
+ // szövegében. A halmaz-átfedés ezt nem látja (neki a szórt egyezés ugyanannyi) — pedig egy bemásolt
159
+ // hibaüzenet vagy idézett mondat majdnem biztos relevancia-jel. Az indoklás + a mért eset a
160
+ // `MIN_PHRASE_LEN` konstans doc-commentjében.
161
+ let phraseBonus = 0;
162
+ if (queryPhraseTokens && queryPhraseTokens.length) {
163
+ const searchable = FAM_LexicalMatch_Util.normalizeForPhrase(`${entry.content ?? ''} ${(entry.headingPath ?? []).join(' ')} ${entry.sourceFilePath ?? ''}`);
164
+ const run = FAM_LexicalMatch_Util.longestPhraseRun(queryPhraseTokens, searchable);
165
+ if (run > 0) {
166
+ const considered = Math.min(queryPhraseTokens.length, FAM_LexicalMatch_Util.MAX_PHRASE_TOKENS);
167
+ phraseBonus = FAM_LexicalMatch_Util.PHRASE_BONUS * (run / considered);
168
+ }
169
+ }
170
+ return Math.min(1, base + titleBonus + phraseBonus);
98
171
  }
99
172
  }
100
173
  exports.FAM_LexicalMatch_Util = FAM_LexicalMatch_Util;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /**
3
+ * SZÓ SZERINTI IDÉZET-MENTÉS — a döntés, hogy egy query megéri-e a pontos szöveg-keresést (2026-09-02).
4
+ *
5
+ * **A mért hiba.** A black-box futás leletét külön ellenőrizve: a
6
+ * `402 Payment Required - You must sign up for private packages` hibaüzenet **szó szerinti bemásolása** —
7
+ * a legtermészetesebb fejlesztői reflex — a szabályt nem hozta be. Az első hipotézisem (rangsor-hiba) a
8
+ * mérésen **MEGBUKOTT**: a 60 visszaadott találatból **egy sem tartalmazta a „402"-t**, vagyis a mondatot
9
+ * hordozó chunk **be sem került a jelöltek közé**. Ez **recall-hiba**: egy kontextus nélküli hibaüzenet
10
+ * embeddingje távol esik a szabály-próza embeddingjétől, és amit a vektor-réteg nem hoz be, azt semmilyen
11
+ * rangsor-javítás nem érheti el.
12
+ *
13
+ * **A megoldás mintája MÁR LÉTEZIK:** a FEAT-003 kód-expanzió (`applyCodeExpansion`) pontosan ezt csinálja
14
+ * azonosító-kódokra — ha a query kódot tartalmaz, a citáló elemek determinisztikusan bekerülnek, a
15
+ * vektor-rangsortól függetlenül. Ez a util ugyanezt terjeszti ki a **szó szerinti idézetre**.
16
+ *
17
+ * **Miért kell szigorú kapu.** A mentés Mongo-oldali szöveg-keresés, ami a nagy tárakon collection-scan.
18
+ * Mérve (2026-09-02): `rules` 296 ms · `coding_patterns` 504 ms · `knowledge` 2,1 s · `memory` 4,9 s ·
19
+ * `documents` 13,1 s · `codebase` 45,2 s. Ezért a mentés (1) csak IDÉZET-SZERŰ query-re fut, és (2) csak
20
+ * a `read.literalRescueMaxDocs` alatti méretű tárakon.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.FAM_LiteralPhrase_Util = void 0;
24
+ /** A minimális token-szám: ennél rövidebb részlet nem idézet, hanem véletlen szó-egybeesés. */
25
+ const MIN_TOKENS = 5;
26
+ /** A maximális token-szám: a hosszú promptot levágjuk (a regex-költség és a találhatóság miatt is). */
27
+ const MAX_TOKENS = 16;
28
+ /**
29
+ * A „disztinktív" token minimális hossza. Egy csupa rövid, köznapi szóból álló mondat (`mi a teendo ha a
30
+ * build elszall`) nem idézet — az a normál szemantikus keresés dolga. Számjegyet tartalmazó token (hibakód,
31
+ * verzió, azonosító) MINDIG disztinktívnek számít.
32
+ */
33
+ const DISTINCTIVE_LEN = 7;
34
+ class FAM_LiteralPhrase_Util {
35
+ /**
36
+ * IDÉZET-SZERŰ-e a query, és ha igen, mit keressünk. `undefined` = nem éri meg a szöveg-keresés.
37
+ *
38
+ * **Ismert korlát, nyíltan:** a keresett szöveg a query ELEJÉTŐL vett, `MAX_TOKENS`-ig tartó összefüggő
39
+ * részlet — vagyis a TISZTÁN bemásolt idézetre működik. Ha a felhasználó saját mondatba ágyazza az
40
+ * idézetet („npm publish failed with 402 … what should I do"), a szó szerinti egyezés elmarad, és a
41
+ * mentés nem talál. Az a hibaosztály viszont **mérve nem is szorul mentésre**: ott a normál vektor-út
42
+ * behozza a szabályt (#2 helyezés). A mentés a lyukat foltozza, nem a működő utat írja át.
43
+ */
44
+ static extract(query) {
45
+ const tokens = String(query ?? '')
46
+ .toLowerCase()
47
+ .split(/[^a-z0-9áéíóöőúüű]+/)
48
+ .filter((token) => token.length >= 2);
49
+ if (tokens.length < MIN_TOKENS) {
50
+ return undefined;
51
+ }
52
+ const window = tokens.slice(0, MAX_TOKENS);
53
+ if (!window.some((token) => FAM_LiteralPhrase_Util.isDistinctive(token))) {
54
+ return undefined;
55
+ }
56
+ return { phrase: window.join(' '), tokenCount: window.length };
57
+ }
58
+ /** Disztinktív-e a token: számjegyet tartalmaz (hibakód/verzió/azonosító) VAGY elég hosszú. */
59
+ static isDistinctive(token) {
60
+ return /[0-9]/.test(token) || token.length >= DISTINCTIVE_LEN;
61
+ }
62
+ /**
63
+ * A szó szerinti kereséshez használt regex: a tokenek között TETSZŐLEGES nem-alfanumerikus elválasztó
64
+ * állhat, mert a korpuszban a mondat írásjelekkel, sortöréssel, backtickkel tagolt (`402 Payment
65
+ * Required — You must…`). A tokenek escape-elve — a query SOHA nem tud regexet injektálni.
66
+ */
67
+ static toRegexSource(phrase) {
68
+ return phrase
69
+ .split(' ')
70
+ .map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
71
+ .join('[^a-z0-9áéíóöőúüű]+');
72
+ }
73
+ }
74
+ exports.FAM_LiteralPhrase_Util = FAM_LiteralPhrase_Util;
@@ -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
@@ -12,6 +12,7 @@ const fam_retrieval_candidate_data_service_1 = require("./fam-retrieval-candidat
12
12
  const fam_retrieval_suggestions_util_1 = require("./fam-retrieval-suggestions.util");
13
13
  const fam_memory_activation_util_1 = require("../_collections/fam-memory-activation.util");
14
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");
15
16
  const fam_result_diversity_util_1 = require("../_collections/fam-result-diversity.util");
16
17
  const fam_memory_reactivation_control_service_1 = require("./fam-memory-reactivation.control-service");
17
18
  const fam_memory_cold_search_control_service_1 = require("./fam-memory-cold-search.control-service");
@@ -100,7 +101,12 @@ class FAM_Retrieval_ControlService {
100
101
  // FEAT-006 context-injection deduper: a hívó által MÁR injektált `_id`-k kizárása (a minScore/topK ELŐTT,
101
102
  // hogy ne foglaljanak helyet) — egy session ne kapja vissza ugyanazt a kontextust kétszer. A kizárás a
102
103
  // kód-exact-re IS vonatkozik (ha a session már birtokolja, nem kell újra, F3-1 ellenére sem).
103
- 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);
104
110
  // ⑤ weight-szorzó MÁR a hit-eken (finalScore). ⑥ minScore-küszöb → relevantSet — DE F3-2: a kód-exact
105
111
  // citáció (`matchedCodes` nem-üres) BYPASS-olja a minScore-t (a determinisztikusan citált kód eleme
106
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).
@@ -125,7 +131,9 @@ class FAM_Retrieval_ControlService {
125
131
  }
126
132
  // dsgn-013 §4 (MAM F2) — FELIDÉZÉS-megerősítés: a ténylegesen VISSZAADOTT `memory`-hitek „új energiát kapnak"
127
133
  // (lastRecalledAt=now, recallCount++, dormant=false). ASYNC fire-and-forget — NEM blokkolja a read választ.
128
- this.reactivateReturnedMemoryHits(result.hits, config);
134
+ if (!query.skipReactivation) {
135
+ this.reactivateReturnedMemoryHits(result.hits, config);
136
+ }
129
137
  return result;
130
138
  }
131
139
  /**
@@ -146,7 +154,7 @@ class FAM_Retrieval_ControlService {
146
154
  return undefined;
147
155
  }
148
156
  const asked = new Set(set.query.tables);
149
- const probeTables = [fam_table_type_enum_1.FAM_Table.memory, fam_table_type_enum_1.FAM_Table.knowledge]
157
+ const probeTables = set.config.crossTableNoteProbeTables
150
158
  .filter((table) => !asked.has(table));
151
159
  if (!probeTables.length) {
152
160
  return undefined;
@@ -188,10 +196,18 @@ class FAM_Retrieval_ControlService {
188
196
  async probeTable(table, set) {
189
197
  const tableConfig = await this.resolveReadConfig(table, set.query);
190
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.
191
- const probeConfig = { ...tableConfig, topK: set.config.crossTableNoteTopK, minScore: 0 };
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
+ };
192
206
  const hits = await this.searchTable({
193
207
  table: table, query: set.query, queryVector: set.queryVector,
194
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),
195
211
  });
196
212
  return hits.slice(0, set.config.crossTableNoteTopK).map((hit) => {
197
213
  // A `kind`/`tags`/`scope` NEM dekoráció: az MCP-n ÍRT memória-bejegyzésnek nincs fájl-útja
@@ -278,6 +294,57 @@ class FAM_Retrieval_ControlService {
278
294
  }
279
295
  return Array.from(byId.values());
280
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
+ }
281
348
  /**
282
349
  * Egy kód-citáló entry beolvasztása a hit-halmazba (FEAT-003). `score=1.0` (exact citation = maximális
283
350
  * relevancia), `finalScore=1.0×weight` (a meglévő weight-modellbe illesztve). `weight≤0` → kizárt
@@ -310,6 +377,37 @@ class FAM_Retrieval_ControlService {
310
377
  set.byId.set(id, this.toCodeHit({ entry: set.entry, id, table: set.table, weight, matched, includeContent: set.includeContent }));
311
378
  }
312
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
+ }
313
411
  toCodeHit(set) {
314
412
  const lastModified = set.entry.__lastModified;
315
413
  return {
@@ -365,7 +463,7 @@ class FAM_Retrieval_ControlService {
365
463
  vectorHits = await embedding_1.FAM_VectorSearch_ControlService.getInstance().search({
366
464
  table: set.table,
367
465
  queryVector: set.queryVector,
368
- topK: exports.MAX_PAGE_SIZE,
466
+ topK: set.vectorTopK ?? exports.MAX_PAGE_SIZE,
369
467
  });
370
468
  if (!vectorHits.length) {
371
469
  return [];
@@ -401,12 +499,15 @@ class FAM_Retrieval_ControlService {
401
499
  table: set.table,
402
500
  queryVector: set.queryVector,
403
501
  candidateIds: candidateIds,
404
- topK: Math.min(candidateIds.length, exports.MAX_PAGE_SIZE),
502
+ topK: Math.min(candidateIds.length, set.vectorTopK ?? exports.MAX_PAGE_SIZE),
405
503
  });
406
504
  }
407
505
  // ⑤ weight-szorzó: finalScore = score × (weight ?? 1); weight ≤ 0 → kizárt (nem kerül a hits közé).
408
506
  // Improvement 2: a query lexikai term-jeit EGYSZER tokenizáljuk (a hibrid keyword-boost-hoz a toHit-ban).
409
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);
410
511
  const hits = [];
411
512
  for (const vectorHit of vectorHits) {
412
513
  const entry = byId.get(vectorHit.id);
@@ -419,6 +520,7 @@ class FAM_Retrieval_ControlService {
419
520
  }
420
521
  hits.push(this.toHit({
421
522
  entry, vectorHit, table: set.table, weight, queryTerms: queryTerms,
523
+ queryPhraseTokens: queryPhraseTokens,
422
524
  importChunkWeight: set.config.importChunkWeight, importSourceWeight: set.config.importSourceWeight,
423
525
  includeContent: set.includeContent, config: set.config, nowMs: Date.now(),
424
526
  }));
@@ -452,6 +554,7 @@ class FAM_Retrieval_ControlService {
452
554
  });
453
555
  const seen = new Set(hits.map((hit) => hit.id));
454
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);
455
558
  for (const cold of coldHits) {
456
559
  const id = cold.entry._id;
457
560
  const weight = cold.entry.weight ?? 1;
@@ -460,6 +563,7 @@ class FAM_Retrieval_ControlService {
460
563
  }
461
564
  hits.push(this.toHit({
462
565
  entry: cold.entry, vectorHit: { id: id, score: cold.score }, table: set.table, weight: weight, queryTerms: queryTerms,
566
+ queryPhraseTokens: queryPhraseTokens,
463
567
  importChunkWeight: set.config.importChunkWeight, importSourceWeight: set.config.importSourceWeight,
464
568
  includeContent: set.includeContent, config: set.config, nowMs: Date.now(),
465
569
  }));
@@ -555,7 +659,7 @@ class FAM_Retrieval_ControlService {
555
659
  const lexicalScore = set.config.keywordWeight > 0
556
660
  ? fam_lexical_match_util_1.FAM_LexicalMatch_Util.score(set.queryTerms, {
557
661
  content: set.entry.content, headingPath: set.entry.headingPath, sourceFilePath: set.entry.source?.path,
558
- })
662
+ }, set.queryPhraseTokens)
559
663
  : 0;
560
664
  const keywordBoost = 1 + set.config.keywordWeight * lexicalScore;
561
665
  // PROJEKT-SZINTU szorzo a `.famconfig.json`-bol — SZANDEKOSAN a temperalason KIVUL. A `weight` mezo az
@@ -817,11 +921,23 @@ class FAM_Retrieval_ControlService {
817
921
  const memoryMaxFrequencyBoost = await this.resolveNumber(config, 'memory.maxFrequencyBoost', ctx, 2.5);
818
922
  const memoryActivationWeight = await this.resolveNumber(config, 'memory.activationWeight', ctx, 0.35);
819
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);
820
929
  const crossTableNoteValue = (await config.resolve('read.crossTableNoteEnabled', ctx)).value;
821
930
  const crossTableNoteEnabled = crossTableNoteValue !== false;
822
931
  const crossTableNoteMinDelta = await this.resolveNumber(config, 'read.crossTableNoteMinDelta', ctx, 0.025);
823
932
  const crossTableNoteMinScore = await this.resolveNumber(config, 'read.crossTableNoteMinScore', ctx, 0.45);
824
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);
825
941
  const memoryReactivateValue = (await config.resolve('memory.reactivateOnRead', ctx)).value;
826
942
  const memoryReactivateOnRead = memoryReactivateValue !== false;
827
943
  const memoryColdSearchValue = (await config.resolve('memory.coldSearchEnabled', ctx)).value;
@@ -831,10 +947,14 @@ class FAM_Retrieval_ControlService {
831
947
  return {
832
948
  topK: Math.min(topKDefault, exports.MAX_PAGE_SIZE),
833
949
  minScore: minScore,
950
+ literalRescueEnabled: literalRescueEnabled,
951
+ literalRescueMaxDocs: literalRescueMaxDocs,
952
+ literalRescueLimit: literalRescueLimit,
834
953
  crossTableNoteEnabled: crossTableNoteEnabled,
835
954
  crossTableNoteMinDelta: crossTableNoteMinDelta,
836
955
  crossTableNoteMinScore: crossTableNoteMinScore,
837
956
  crossTableNoteTopK: crossTableNoteTopK,
957
+ crossTableNoteProbeTables: crossTableNoteProbeTables.length ? crossTableNoteProbeTables : [fam_table_type_enum_1.FAM_Table.memory],
838
958
  relevanceFloor: relevanceFloor,
839
959
  importChunkWeight: importChunkWeight,
840
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
@@ -70,6 +70,7 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
70
70
  this.ingestRunsEndpoint(),
71
71
  this.duplicatesEndpoint(),
72
72
  this.duplicatesPruneEndpoint(),
73
+ this.memoryRetireEndpoint(),
73
74
  this.configGetEndpoint(),
74
75
  this.configSetEndpoint(),
75
76
  this.scanStartEndpoint(),
@@ -522,22 +523,26 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
522
523
  continue;
523
524
  }
524
525
  const collection = this.entryCollection(registryEntry);
525
- const count = await collection.countDocuments({});
526
- // Embedding-státusz bontás Mongo-aggregációval ($group), NEM a doc-ok memóriába töltésével.
526
+ // FIX (2026-09-02, COLLSCAN → index): a korábbi `countDocuments({})` + `$group embeddingStatus`
527
+ // TELJES collection-átolvasás volt (350k+ doc a nagy tárakon) — meleg Mongo-cache mellett
528
+ // észrevétlen, kihűlt cache + telített lemez mellett viszont 20–50 s/tár, és a `/stats` a UI
529
+ // landing alatt PERCEKIG lógott (mérve: 120 s+ timeout). Most:
530
+ // • darabszám: `estimatedDocumentCount` (collection-metaadat, ~0 ms — a stats-kártyának a
531
+ // nagyságrend kell, nem a tranzakció-pontos szám),
532
+ // • státusz-bontás: per-státusz `countDocuments({embeddingStatus})` — az indexből számol
533
+ // (COUNT_SCAN), doc-olvasás nélkül,
534
+ // • utolsó módosítás: a friss `__lastModified` index tetejéről egyetlen doc.
535
+ const count = await collection.estimatedDocumentCount();
527
536
  const byStatus = { pending: 0, completed: 0, error: 0 };
528
- const grouped = await collection
529
- .aggregate([{ $group: { _id: '$embeddingStatus', n: { $sum: 1 } } }])
530
- .toArray();
531
- for (const group of grouped) {
532
- const status = group._id ?? 'pending';
533
- if (status in byStatus) {
534
- byStatus[status] += group.n;
535
- }
536
- else {
537
- byStatus.pending += group.n;
538
- }
539
- }
540
- // Utolsó módosítás: EGYETLEN doc (a legfrissebb), csak az időbélyeg-mezőre projektálva.
537
+ const [completed, error] = await Promise.all([
538
+ collection.countDocuments({ embeddingStatus: 'completed' }),
539
+ collection.countDocuments({ embeddingStatus: 'error' }),
540
+ ]);
541
+ byStatus.completed = completed;
542
+ byStatus.error = error;
543
+ // A `pending` a maradék (beleértve a státusz-mentes régi doc-okat) — így nem kell harmadik scan.
544
+ byStatus.pending = Math.max(0, count - completed - error);
545
+ // Utolsó módosítás: EGYETLEN doc a `__lastModified` index tetejéről (2026-09-02 óta indexelt).
541
546
  const newest = await collection
542
547
  .find({}, { projection: { __lastModified: 1 } })
543
548
  .sort({ __lastModified: -1 })
@@ -658,6 +663,32 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
658
663
  });
659
664
  }
660
665
  // =========================================================================
666
+ // /memory/retire-temporary — az alvó törmelék nyugdíjazása (dry-run az alapértelmezés)
667
+ // =========================================================================
668
+ /**
669
+ * `POST /memory/retire-temporary` — az alvó beszélgetés-törmelék (`user_prompt` / `temporary_context`)
670
+ * nyugdíjazása a `memory` táron: `superseded + retiredAt`, NEM törlés. A szabály és az indoklás a
671
+ * `fam-memory-retention.util.ts`-ben. Body: `dryRun` (DEFAULT `true`), `retireAfterDays` (override).
672
+ */
673
+ memoryRetireEndpoint() {
674
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
675
+ name: 'memoryRetireTemporary',
676
+ type: fsm_dynamo_1.DyFM_HttpCallType.post,
677
+ endpoint: '/memory/retire-temporary',
678
+ tasks: [
679
+ async (req, res) => {
680
+ await this.run(res, async () => {
681
+ const body = req.body ?? {};
682
+ return retrieval_1.FAM_MemoryRetention_ControlService.getInstance().retire({
683
+ dryRun: body.dryRun !== false,
684
+ retireAfterDays: typeof body.retireAfterDays === 'number' ? body.retireAfterDays : undefined,
685
+ });
686
+ });
687
+ },
688
+ ],
689
+ });
690
+ }
691
+ // =========================================================================
661
692
  // /config — scoped config olvasás/írás (dsgn-007; a UI settings + CLI config)
662
693
  // =========================================================================
663
694
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futdevpro/fdp-agent-memory",
3
- "version": "1.1.177",
3
+ "version": "1.1.194",
4
4
  "description": "Local-first, vector-backed multi-table agent memory exposed as an MCP server (read/write/capabilities). Public, FDP-Templates-free, no auth.",
5
5
  "private": false,
6
6
  "publishConfig": {