@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futdevpro/fdp-agent-memory",
3
- "version": "1.1.166",
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": {
@@ -19,7 +19,13 @@
19
19
  "build",
20
20
  "client-dist",
21
21
  "README.md",
22
- "LICENSE"
22
+ "LICENSE",
23
+ "!build/**/*.spec.js",
24
+ "!build/**/*.spec.js.map",
25
+ "!build/**/*.spec.d.ts",
26
+ "!build/**/_benchmarks/**",
27
+ "!build/**/_integration-tests/**",
28
+ "!build/**/_spec-support/**"
23
29
  ],
24
30
  "engines": {
25
31
  "node": ">=20"
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_PruneDuplicates_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 prune-duplicates [--table t] [--apply]` — BÁJTAZONOS ismétlés-nyesés.
10
+ *
11
+ * Az azonos `(absolutePath, chunkIndex, chunkTotal, contentHash)` négyesű példányok között NINCS
12
+ * információ-különbség: a legfrissebbet megtartjuk, a többit soft-delete-eljük, és a csoport
13
+ * `recallCount` / `lastRecalledAt` MAXIMUMÁT átvisszük a megtartottra (a felidézés-történet nem veszhet el).
14
+ *
15
+ * **Alapértelmezés a DRY-RUN** — a törléshez explicit `--apply` kell. A `find-duplicates`-szel NEM
16
+ * keverendő: az SZEMANTIKAI hasonlóságot keres (koszinusz-cluster) és szándékosan sosem töröl.
17
+ *
18
+ * `superseded` bejegyzést tartalmazó csoporthoz SOHA nem nyúl (az a `memory` tár szándékos, leváltott
19
+ * generációja) — a kihagyás számláltan látszik a riportban.
20
+ *
21
+ * ⏱️ **FUTÁSIDŐ (mérve 2026-09-01, 555k aktív chunk):** a TELJES korpusz **~123 s**, egyetlen tár (`rules`)
22
+ * **~1,4 s**. Ez egy hosszan futó, szinkron aggregáció — automatizált hívónál (CI, HTTP-kliens) vagy emeld
23
+ * a timeoutot, vagy `--table`-lel bontsd táranként. A `--max-groups` a MUNKÁT nem, csak a feldolgozott
24
+ * csoportok számát korlátozza (az aggregáció akkor is végigmegy).
25
+ */
26
+ class FAM_PruneDuplicates_Util {
27
+ /** A `prune-duplicates` parancs-deszkriptor. */
28
+ static command() {
29
+ const command = new commander_1.Command('prune-duplicates');
30
+ command
31
+ .description('Bájtazonos ismétlés-nyesés (a legfrissebb marad; DRY-RUN az alapértelmezés)')
32
+ .option('--table <table>', 'csak ez a tár (default: MINDEN tár)')
33
+ .option('--apply', 'a nyesés TÉNYLEGES végrehajtása (enélkül csak mér, nem ír)')
34
+ .option('--max-groups <n>', 'csoport-plafon egy futásra (default 50000; a capped jelzi, ha több)')
35
+ .action(async () => {
36
+ fam_output_util_1.FAM_Output_Util.exit(await FAM_PruneDuplicates_Util.run(command));
37
+ });
38
+ return command;
39
+ }
40
+ /** A `prune-duplicates` futtatása a REST `POST /api/duplicates/prune`-on. */
41
+ static async run(command) {
42
+ const globals = command.optsWithGlobals();
43
+ const local = command.opts();
44
+ const client = new fam_client_service_1.FAM_CliClient_Service(globals.serverUrl);
45
+ const maxGroups = local.maxGroups !== undefined ? Number(local.maxGroups) : undefined;
46
+ const result = await client.post('/api/duplicates/prune', {
47
+ table: local.table,
48
+ // A `--apply` HIÁNYA a biztonságos irány: dryRun marad. Elgépelt kapcsoló sosem töröl.
49
+ dryRun: local.apply !== true,
50
+ maxGroups: Number.isFinite(maxGroups) ? maxGroups : undefined,
51
+ });
52
+ if (!result.ok) {
53
+ return fam_output_util_1.FAM_Output_Util.failure({
54
+ options: globals,
55
+ command: 'prune-duplicates',
56
+ error: result.error ?? { errorCode: 'FAM-CLI-PRUNE-001', message: 'Ismeretlen nyesési hiba.' },
57
+ exitCode: result.unreachable ? fam_cli_const_1.FAM_CliExitCode.serverUnreachable : fam_cli_const_1.FAM_CliExitCode.operationError,
58
+ });
59
+ }
60
+ return fam_output_util_1.FAM_Output_Util.success({
61
+ options: globals,
62
+ command: 'prune-duplicates',
63
+ data: result.data,
64
+ humanFormatter: (data) => FAM_PruneDuplicates_Util.format(data),
65
+ });
66
+ }
67
+ /** Ember-olvasható összegzés: per-tár sorok + összesítés + a dry-run explicit jelzése. */
68
+ static format(report) {
69
+ const lines = [];
70
+ lines.push(report.dryRun
71
+ ? 'Bájtazonos ismétlés-nyesés — DRY-RUN (semmi nem változott; a végrehajtáshoz: --apply):'
72
+ : 'Bájtazonos ismétlés-nyesés — VÉGREHAJTVA:');
73
+ for (const row of report.tables) {
74
+ if (!row.groups) {
75
+ continue;
76
+ }
77
+ lines.push(` ${row.table.padEnd(18)} ${String(row.groups).padStart(6)} csoport`
78
+ + ` → ${String(row.prunedChunks).padStart(6)} chunk`
79
+ + `${row.recallCarried ? `, ${row.recallCarried} felidézés-történet átvive` : ''}`
80
+ + `${row.skippedSuperseded ? `, ${row.skippedSuperseded} superseded KIHAGYVA` : ''}`
81
+ + `${row.capped ? ' ⚠ CAPPED (emeld a --max-groups-ot, vagy futtasd újra)' : ''}`);
82
+ }
83
+ if (!report.totalGroups) {
84
+ lines.push(' ✓ nincs bájtazonos ismétlés.');
85
+ return lines.join('\n');
86
+ }
87
+ lines.push(` ÖSSZESEN: ${report.totalGroups} csoport, ${report.totalPrunedChunks} felesleges chunk`
88
+ + `, ${report.totalRecallCarried} felidézés-történet átvive`
89
+ + `, ${report.totalSkippedSuperseded} superseded csoport érintetlen.`);
90
+ return lines.join('\n');
91
+ }
92
+ }
93
+ exports.FAM_PruneDuplicates_Util = FAM_PruneDuplicates_Util;
@@ -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;
@@ -8,6 +8,8 @@ const scan_projects_command_1 = require("./_commands/scan-projects.command");
8
8
  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
+ const prune_duplicates_command_1 = require("./_commands/prune-duplicates.command");
12
+ const retire_temporary_command_1 = require("./_commands/retire-temporary.command");
11
13
  const errors_command_1 = require("./_commands/errors.command");
12
14
  const init_command_1 = require("./_commands/init.command");
13
15
  const doctor_command_1 = require("./_commands/doctor.command");
@@ -56,6 +58,8 @@ function registerFAMCommands(program, version) {
56
58
  remote_command_1.FAM_Remote_Util.command(),
57
59
  stats_command_1.FAM_Stats_Util.command(),
58
60
  find_duplicates_command_1.FAM_FindDuplicates_Util.command(),
61
+ prune_duplicates_command_1.FAM_PruneDuplicates_Util.command(),
62
+ retire_temporary_command_1.FAM_RetireTemporary_Util.command(),
59
63
  errors_command_1.FAM_Errors_Util.command(),
60
64
  // 7 delegáló alias (README copy-paste-elhetőség, nincs külön logika; dsgn-010 §10).
61
65
  doctor_command_1.FAM_Doctor_Util.aliasCommand('validate-env', ['env']),
@@ -187,6 +187,66 @@ exports.CONFIG_CATALOG = {
187
187
  + 'Mert indok (2026-08-22): kontrollalt teszten a rules tar 0/3 aranyban tunt el a documents mogott, '
188
188
  + 'holott a csak-rules query azonnal hozta a kanonikus szabalyt.',
189
189
  },
190
+ // KERESZT-TAR "mellesleg"-javaslat (2026-09-01). A `memory`/`knowledge` a HIBAKERESES-jellegu kerdeseken
191
+ // eros ("miert tort el", "lattuk mar ezt"), de az agent jellemzoen `codebase`/`documents`-ben keres, mert
192
+ // nem tudja, hogy ott van. Merve 15 realisztikus kerdesen: 13-nal az ELSODLEGES tar volt jobb -> a talalat
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
+ },
215
+ 'read.crossTableNoteEnabled': {
216
+ type: 'boolean', default: true, levels: ALL_LEVELS,
217
+ description: 'Kereszt-tar "mellesleg"-javaslat: ha a memory/knowledge taron ERDEMBEN erosebb talalat van, '
218
+ + 'a valasz kap egy kulon `crossTableNote` mezot. A `hits` VALTOZATLAN marad (sem sorrend, sem tagsag). '
219
+ + 'Nem fut, ha a hivo eleve kerte ezeket a tarakat. Merve: +376 ms (+14%) a szonda koltsege.',
220
+ },
221
+ 'read.crossTableNoteMinDelta': {
222
+ type: 'number', default: 0.025, min: 0, max: 1, levels: ALL_LEVELS,
223
+ description: 'A megszolalashoz szukseges NYERS score-elony. 0 = kikapcsolva. Kalibralva 44 eseten, '
224
+ + 'KONTROLLCSOPORTTAL (24 strukturalis kerdes + 20 diagnosztikai). FONTOS: az elso kalibracio '
225
+ + '0,12-t adott, de kiderult, hogy a magas deltakat szinte kizarolag beszelgetes-tormelek '
226
+ + '(user_prompt / temporary_context, a memory tar 20,2%-a) termelte. A tormelek kizarasa utan '
227
+ + '0,12-nel a jegyzet SOHA nem szolalt volna meg. A valos jel a 0,025-0,06 savban van, es ott '
228
+ + 'pontos: 3/44 megszolalas (6,8%), koztuk a docker-cache-token es a lokal-vs-CI tanulsag. '
229
+ + 'KORLAT: ket kulonbozo tartalomtipus nyers koszinuszat hasonlitja -> korpusz-valtaskor UJRA KELL MERNI '
230
+ + '(`node build/src/_benchmarks/fam-crosstable-bench.js`).',
231
+ },
232
+ 'read.crossTableNoteMinScore': {
233
+ type: 'number', default: 0.45, min: 0, max: 1, levels: ALL_LEVELS,
234
+ description: 'Abszolut also korlat a jelolt sajat score-jara: gyenge talalatot akkor sem ajanlunk, '
235
+ + 'ha az elsodleges tar meg gyengebb (kulonben a "nincs talalat" eset minden alkalommal zajt szulne).',
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
+ },
245
+ 'read.crossTableNoteTopK': {
246
+ type: 'number', default: 3, min: 1, max: 10, integer: true, levels: ALL_LEVELS,
247
+ description: 'A szonda topK-ja taranként. Kicsi: a szonda csak azt donti el, VAN-E erdemben erosebb — '
248
+ + 'nem masodik talalati listat epit.',
249
+ },
190
250
  'read.relevanceFloor': {
191
251
  type: 'number', default: 0.60, min: 0.0, max: 1.0, levels: ALL_LEVELS,
192
252
  // PER-TÁR kalibráció (2026-08-14 MÉRÉS): a tárak score-tartománya ELTÉR — a `codebase`/`documents` szimbólum-
@@ -329,6 +389,21 @@ exports.CONFIG_CATALOG = {
329
389
  description: 'A cold-search dormant-scan FELSŐ KORLÁTJA (biztonsági memóriakorlát, dsgn-013 §5.3): a Mongo-koszinusz '
330
390
  + 'legfeljebb ennyi dormant entry vektorát tölti be egy keresésnél. Default 5000.',
331
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
+ },
332
407
  'memory.supersededPenalty': {
333
408
  type: 'number', default: 0.25, min: 0, max: 1, levels: ALL_LEVELS,
334
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
  });
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * PONTOS (bájtazonos) ismétlés-nyesés — a döntési logika, tisztán és tesztelhetően.
4
+ *
5
+ * **Miben más ez, mint a `FAM_DuplicateScan_ControlService`?** Az a SZEMANTIKAI hasonlóságot keresi
6
+ * (koszinusz-cluster) és szándékosan sosem töröl — ott emberi ítélet kell. EZ viszont a BIZONYÍTOTTAN
7
+ * AZONOS újra-beolvasást nyesi: azonos `(source.absolutePath, chunkIndex, chunkTotal, contentHash)` →
8
+ * a példányok között NINCS információ-különbség, így az elhagyásuk nem veszteség.
9
+ *
10
+ * **Mért indok (2026-09-01):** egy projekt-átnevezés után a heti re-scan cél-uniója MINDKÉT nevet
11
+ * megtartotta ugyanarra a mappára, a delta-összevetés pedig a levél-scopeId-re szűrt — így a második
12
+ * menet üres `existing`-et kapott, és mindent `new`-ként vitt be. Az aktív korpuszban ez **13 201**
13
+ * felesleges chunk (a 555 102 aktívból 2,38%). A gyökér-okot a scope-vak fallback + az útvonal-dedup
14
+ * javítja; EZ a maradék takarítása.
15
+ *
16
+ * **Amihez SOHA nem nyúl:**
17
+ * - `superseded` bejegyzés — az a `memory` tár szándékos, leváltott generációja (dsgn-013 MAM F4),
18
+ * az owner explicit kérése szerint MEGMARAD („nem baj ha fennmarad olyan memória amit a Claude már
19
+ * elfelejtett… sőt!"). Mérve: a `memory` táron 0 bájtazonos csoport van, mind a 287 eltérő tartalmú.
20
+ * - ELTÉRŐ `contentHash`-ű példányok — azok valódi tartalom-történet, nem ismétlés (mérve 3 046 csoport).
21
+ *
22
+ * **Megőrzött felidézés-történet:** a csoport `recallCount` / `lastRecalledAt` MAXIMUMA a megtartott
23
+ * példányra vándorol — az owner „amit többet használtunk, az tovább él" elve nem sérülhet a nyesésben.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.FAM_ExactDuplicatePrune_Util = void 0;
27
+ class FAM_ExactDuplicatePrune_Util {
28
+ /**
29
+ * A megtartandó példány: a LEGFRISSEBB `lastModified`. Döntetlennél a lexikálisan legnagyobb `id`
30
+ * nyer — nem esztétika, hanem DETERMINIZMUS: a nyesés kétszer futtatva ugyanazt kell adja, különben
31
+ * a második futás megint mozgatna, és a művelet nem lenne idempotens.
32
+ */
33
+ static chooseKeeper(members) {
34
+ if (!members.length) {
35
+ return undefined;
36
+ }
37
+ return [...members].sort((a, b) => {
38
+ const at = typeof a.lastModified === 'number' ? a.lastModified : 0;
39
+ const bt = typeof b.lastModified === 'number' ? b.lastModified : 0;
40
+ return bt !== at ? bt - at : (a.id < b.id ? 1 : -1);
41
+ })[0];
42
+ }
43
+ /** A csoport felidézés-történetének maximuma (a megtartottra vitt érték bemenete). */
44
+ static maxRecall(members) {
45
+ let count = 0;
46
+ let at = 0;
47
+ for (const member of members) {
48
+ if (typeof member.recallCount === 'number' && member.recallCount > count) {
49
+ count = member.recallCount;
50
+ }
51
+ if (typeof member.lastRecalledAt === 'number' && member.lastRecalledAt > at) {
52
+ at = member.lastRecalledAt;
53
+ }
54
+ }
55
+ return { recallCount: count, lastRecalledAt: at };
56
+ }
57
+ /**
58
+ * Egy csoport nyesési terve — vagy a kihagyás INDOKA. A `carry` csak azt a mezőt tartalmazza, ami a
59
+ * megtartotton ténylegesen NŐNE: fölösleges írást nem kérünk, és a keeper saját értékét sem rontjuk le.
60
+ */
61
+ static planGroup(members) {
62
+ if (members.length < 2) {
63
+ return 'single-member';
64
+ }
65
+ if (members.some((member) => member.superseded === true)) {
66
+ return 'superseded-present';
67
+ }
68
+ const keeper = FAM_ExactDuplicatePrune_Util.chooseKeeper(members);
69
+ const max = FAM_ExactDuplicatePrune_Util.maxRecall(members);
70
+ const carry = {};
71
+ if (max.recallCount > (keeper.recallCount ?? 0)) {
72
+ carry.recallCount = max.recallCount;
73
+ }
74
+ if (max.lastRecalledAt > (keeper.lastRecalledAt ?? 0)) {
75
+ carry.lastRecalledAt = max.lastRecalledAt;
76
+ }
77
+ return {
78
+ keepId: keeper.id,
79
+ dropIds: members.filter((member) => member.id !== keeper.id)
80
+ .map((member) => member.id),
81
+ carry: carry,
82
+ };
83
+ }
84
+ }
85
+ exports.FAM_ExactDuplicatePrune_Util = FAM_ExactDuplicatePrune_Util;
@@ -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;
@@ -156,6 +156,27 @@ class FAM_Entry_DataService extends nts_dynamo_1.DyNTS_DataService {
156
156
  filterBy['source.root'] = { $ne: root };
157
157
  await this.updateData({ filterBy: filterBy, update: update });
158
158
  }
159
+ /**
160
+ * A `scopePath` FELTÉTELES backfill-je (2026-09-01) — a re-scan `equal` ágán hívva.
161
+ *
162
+ * **Miért kell.** A fájl IDENTITÁSA az abszolút útvonal, a scope csak besorolás — ezért az `equal`
163
+ * összevetés (a scope-vak fallback óta) MÁS scope alatt álló chunkot is adoptál. Enélkül a backfill
164
+ * nélkül az adoptált chunk ÖRÖKRE a RÉGI projekt-név alatt maradna: a duplikáció megszűnne, de a
165
+ * scope-szűrt keresés az ÚJ néven nem találná meg. A régi viselkedés (teljes duplikálás) legalább
166
+ * hagyott egy példányt az új scope alatt — a javítás enélkül CSERÉLNE egy hibát egy másikra.
167
+ *
168
+ * Csak akkor ír, ha a LEVÉL scopeId eltér (no-op, ha már stimmel).
169
+ */
170
+ async backfillScopePath(id, scopePath) {
171
+ if (!scopePath.length) {
172
+ return;
173
+ }
174
+ const leafScopeId = scopePath[scopePath.length - 1].scopeId;
175
+ const update = { $set: { scopePath: scopePath } };
176
+ const filterBy = { _id: id };
177
+ filterBy['scopePath.scopeId'] = { $ne: leafScopeId };
178
+ await this.updateData({ filterBy: filterBy, update: update });
179
+ }
159
180
  /**
160
181
  * A `sourceFilePath` (display-relatív út) FELTÉTELES backfill-je (FAM-REV-050) — a re-scan `equal` ágán
161
182
  * hívva, hogy a default (basePath-nélküli) relatív út KONZISZTENS legyen, miután a fájl-identitás az
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_ExactDuplicatePrune_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_entry_data_service_1 = require("./fam-entry.data-service");
8
+ const fam_exact_duplicate_prune_util_1 = require("../_collections/fam-exact-duplicate-prune.util");
9
+ const fam_store_registry_const_1 = require("../_collections/fam-store-registry.const");
10
+ const fam_vector_search_control_service_1 = require("./fam-vector-search.control-service");
11
+ const DEFAULT_MAX_GROUPS = 50000;
12
+ /**
13
+ * BÁJTAZONOS ismétlés-nyesés (2026-09-01). Azonos `(absolutePath, chunkIndex, chunkTotal, contentHash)`
14
+ * → a példányok között NINCS információ-különbség; a legfrissebbet tartjuk, a többit soft-delete-eljük.
15
+ *
16
+ * **Nem keverendő** a `FAM_DuplicateScan_ControlService`-szel: az SZEMANTIKAI hasonlóságot keres és
17
+ * sosem töröl (ott emberi ítélet kell). A kritériumok és a kivételek indoklása a pure utilban áll.
18
+ *
19
+ * A soft-delete DIREKT `_deleted` (nem `deleteData`) — a FAM entry-tárak `addArchive:true`-ok, így a
20
+ * per-entry törlés minden példányról ARCHÍV-másolatot csinálna (bloat + lassú tízezres nagyságrenden).
21
+ * Ugyanez a döntés a scope-reset ágon is (`fam-scope-maintenance.control-service.ts`).
22
+ */
23
+ class FAM_ExactDuplicatePrune_ControlService {
24
+ static _instance;
25
+ static getInstance() {
26
+ if (!FAM_ExactDuplicatePrune_ControlService._instance) {
27
+ FAM_ExactDuplicatePrune_ControlService._instance = new FAM_ExactDuplicatePrune_ControlService();
28
+ }
29
+ return FAM_ExactDuplicatePrune_ControlService._instance;
30
+ }
31
+ /** A nyesés futtatása — `dryRun` esetén CSAK mér, semmit nem ír. */
32
+ async prune(input = {}) {
33
+ const dryRun = input.dryRun !== false;
34
+ const maxGroups = typeof input.maxGroups === 'number' && input.maxGroups > 0
35
+ ? Math.floor(input.maxGroups) : DEFAULT_MAX_GROUPS;
36
+ const targets = input.table
37
+ ? [fam_store_registry_const_1.FAM_StoreRegistry_Util.getEntry(input.table)].filter(Boolean)
38
+ : fam_store_registry_const_1.FAM_STORE_REGISTRY;
39
+ const tables = [];
40
+ for (const registryEntry of targets) {
41
+ const result = await this.pruneTable(registryEntry, dryRun, maxGroups);
42
+ if (result) {
43
+ tables.push(result);
44
+ }
45
+ }
46
+ const sum = (pick) => tables.reduce((acc, row) => acc + pick(row), 0);
47
+ return {
48
+ operation: 'prune_exact_duplicates',
49
+ dryRun: dryRun,
50
+ tables: tables,
51
+ totalGroups: sum((row) => row.groups),
52
+ totalPrunedChunks: sum((row) => row.prunedChunks),
53
+ totalRecallCarried: sum((row) => row.recallCarried),
54
+ totalSkippedSuperseded: sum((row) => row.skippedSuperseded),
55
+ };
56
+ }
57
+ /** Egy tár nyesése. A modell példányosítása regisztrálja a mongoose-modellt (a raw collection eléréséhez). */
58
+ async pruneTable(registryEntry, dryRun, maxGroups) {
59
+ new fam_entry_data_service_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: 'exact-duplicate-prune' });
60
+ const collection = mongoose_1.default.models[registryEntry.dataParams.dataName]?.collection;
61
+ if (!collection) {
62
+ return undefined;
63
+ }
64
+ const groups = await collection.aggregate([
65
+ {
66
+ $match: {
67
+ _deleted: null,
68
+ 'source.absolutePath': { $exists: true, $ne: null },
69
+ chunkTotal: { $gt: 0 },
70
+ contentHash: { $exists: true, $ne: null },
71
+ },
72
+ },
73
+ {
74
+ $group: {
75
+ _id: { path: '$source.absolutePath', index: '$chunkIndex', total: '$chunkTotal', hash: '$contentHash' },
76
+ count: { $sum: 1 },
77
+ members: {
78
+ $push: {
79
+ id: '$_id',
80
+ // A `__lastModified` Date — a tiszta util számot vár, ezért itt konvertáljuk (hiány → null).
81
+ lastModified: { $convert: { input: '$__lastModified', to: 'long', onError: null, onNull: null } },
82
+ recallCount: '$recallCount',
83
+ lastRecalledAt: '$lastRecalledAt',
84
+ superseded: '$superseded',
85
+ },
86
+ },
87
+ },
88
+ },
89
+ { $match: { count: { $gt: 1 } } },
90
+ { $limit: maxGroups + 1 },
91
+ ], { allowDiskUse: true }).toArray();
92
+ const capped = groups.length > maxGroups;
93
+ const work = capped ? groups.slice(0, maxGroups) : groups;
94
+ let prunedChunks = 0;
95
+ let recallCarried = 0;
96
+ let skippedSuperseded = 0;
97
+ for (const group of work) {
98
+ // A nyers `_id`-t megőrizzük (ObjectId VAGY string lehet) — a util string-kulcsot lát, a DB a nyerset kapja.
99
+ const byKey = new Map();
100
+ const members = group.members.map((raw) => {
101
+ const key = String(raw.id);
102
+ byKey.set(key, raw.id);
103
+ return {
104
+ id: key,
105
+ lastModified: typeof raw.lastModified === 'number' ? raw.lastModified : undefined,
106
+ recallCount: raw.recallCount,
107
+ lastRecalledAt: raw.lastRecalledAt,
108
+ superseded: raw.superseded,
109
+ };
110
+ });
111
+ const plan = fam_exact_duplicate_prune_util_1.FAM_ExactDuplicatePrune_Util.planGroup(members);
112
+ if (typeof plan === 'string') {
113
+ if (plan === 'superseded-present') {
114
+ skippedSuperseded++;
115
+ }
116
+ continue;
117
+ }
118
+ prunedChunks += plan.dropIds.length;
119
+ const hasCarry = Object.keys(plan.carry).length > 0;
120
+ if (hasCarry) {
121
+ recallCarried++;
122
+ }
123
+ if (dryRun) {
124
+ continue;
125
+ }
126
+ const dropRaw = plan.dropIds.map((id) => byKey.get(id));
127
+ await collection.updateMany({ _id: { $in: dropRaw } }, { $set: { _deleted: new Date() } });
128
+ if (hasCarry) {
129
+ // A felidézés-történet MAXIMUMA a megtartottra — az owner "amit többet használtunk, az tovább
130
+ // él" elve nem sérülhet attól, hogy épp a ritkábban felidézett példány a legfrissebb.
131
+ await collection.updateOne({ _id: byKey.get(plan.keepId) }, { $set: plan.carry });
132
+ }
133
+ for (const id of plan.dropIds) {
134
+ fam_vector_search_control_service_1.FAM_VectorSearch_ControlService.getInstance().removeVector(registryEntry.table, id);
135
+ }
136
+ }
137
+ if (capped) {
138
+ fsm_dynamo_1.DyFM_Log.warn(`[prune-duplicates] ${registryEntry.table}: a csoport-plafon (${maxGroups}) elerve — `
139
+ + 'a riport `capped: true`-t ad, tovabbi futas szukseges (nincs nema csonkolas).');
140
+ }
141
+ return {
142
+ table: registryEntry.table,
143
+ collection: collection.collectionName,
144
+ groups: work.length,
145
+ prunedChunks: prunedChunks,
146
+ recallCarried: recallCarried,
147
+ skippedSuperseded: skippedSuperseded,
148
+ capped: capped,
149
+ };
150
+ }
151
+ }
152
+ exports.FAM_ExactDuplicatePrune_ControlService = FAM_ExactDuplicatePrune_ControlService;