@futdevpro/fdp-agent-memory 1.1.14 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/package.json +1 -1
- package/build/src/_cli/_commands/config.command.js +4 -1
- package/build/src/_cli/_commands/scan-projects.command.js +26 -1
- package/build/src/_collections/config-catalog.const.js +56 -0
- package/build/src/_collections/fam-config-value-coerce.util.js +63 -0
- package/build/src/_collections/fam-cooldown.util.js +35 -0
- package/build/src/_collections/fam-heap-guard.control-service.js +176 -0
- package/build/src/_collections/title.const.js +31 -0
- package/build/src/_modules/ingest/_collections/fam-famignore.util.js +57 -1
- package/build/src/_modules/ingest/_collections/fam-scan-reconcile.util.js +40 -0
- package/build/src/_modules/ingest/_collections/fam-secret-content.util.js +49 -0
- package/build/src/_modules/ingest/_collections/fam-secret-exclude.util.js +12 -0
- package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +35 -8
- package/build/src/_modules/ingest/_services/fam-scan.control-service.js +77 -11
- package/build/src/_routes/server/api/api.controller.js +38 -2
- package/build/src/app.server.js +31 -1
- package/package.json +1 -1
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futdevpro/fdp-agent-memory",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.21",
|
|
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": {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FAM_Config_Util = void 0;
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
|
+
const fam_config_value_coerce_util_1 = require("../../_collections/fam-config-value-coerce.util");
|
|
5
6
|
const fam_output_util_1 = require("../_collections/fam-output.util");
|
|
6
7
|
const fam_client_service_1 = require("../_services/fam-client.service");
|
|
7
8
|
const fam_cli_const_1 = require("../_collections/fam-cli.const");
|
|
@@ -67,9 +68,11 @@ class FAM_Config_Util {
|
|
|
67
68
|
static async runSet(command) {
|
|
68
69
|
const globals = command.optsWithGlobals();
|
|
69
70
|
const local = command.opts();
|
|
71
|
+
// A CLI `--value` mindig string; a szerver típus-szigorúan validál → a katalógus-típusra konvertáljuk
|
|
72
|
+
// (boolean/number/string[] kulcsok így CLI-ből is állíthatók — FAM_ConfigValueCoerce_Util).
|
|
70
73
|
const body = {
|
|
71
74
|
key: local.key,
|
|
72
|
-
value: local.value,
|
|
75
|
+
value: fam_config_value_coerce_util_1.FAM_ConfigValueCoerce_Util.coerce(local.key, local.value),
|
|
73
76
|
table: local.table,
|
|
74
77
|
scopeId: local.scope,
|
|
75
78
|
};
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FAM_ScanProjects_Util = void 0;
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const commander_1 = require("commander");
|
|
6
|
+
const fam_cooldown_util_1 = require("../../_collections/fam-cooldown.util");
|
|
6
7
|
const fam_output_util_1 = require("../_collections/fam-output.util");
|
|
7
8
|
const fam_client_service_1 = require("../_services/fam-client.service");
|
|
8
9
|
const fam_cli_const_1 = require("../_collections/fam-cli.const");
|
|
@@ -26,6 +27,8 @@ class FAM_ScanProjects_Util {
|
|
|
26
27
|
.option('--org <name>', 'default organization az auto-felderítéshez', 'FutDevPro')
|
|
27
28
|
.option('--table <documents|codebase>', 'default cél-tár', 'codebase')
|
|
28
29
|
.option('--dry-run', 'csak becslés MINDEN projektre, NEM ír', false)
|
|
30
|
+
.option('--cooldown <ms>', 'cooldown a projektek KÖZÉ (ms) — gép-tehermentesítő szünet nagy multi-scanhez; '
|
|
31
|
+
+ 'felülírja a `scan.projectCooldownMs` configot (default: a config, ami 0 = ki)')
|
|
29
32
|
.option('--stop-on-error', 'az első hibánál leáll (default: folytatja a többivel)', false)
|
|
30
33
|
.addHelpText('before', '\n⚠️ TISZTA MULTI-SCAN — ELŐBB minden projektbe IGNORE-FÁJL:\n'
|
|
31
34
|
+ ' Mivel ez TÖBB projektet szkennel egyszerre, a junk gyorsan elszaporodik. MINDEN érintett\n'
|
|
@@ -77,7 +80,11 @@ class FAM_ScanProjects_Util {
|
|
|
77
80
|
const client = new fam_client_service_1.FAM_CliClient_Service(globals.serverUrl);
|
|
78
81
|
const projects = [];
|
|
79
82
|
let okCount = 0;
|
|
80
|
-
|
|
83
|
+
// Projekt-cooldown (user-FR): a `--cooldown <ms>` felülírja a szerver `scan.projectCooldownMs` configját
|
|
84
|
+
// (default 0 = ki). dryRun-on nincs cooldown (nincs valódi embed-munka). A projektek KÖZÉ vár, nem az utolsó után.
|
|
85
|
+
const projectCooldownMs = local.dryRun ? 0 : await FAM_ScanProjects_Util.resolveCooldownMs(client, local.cooldown);
|
|
86
|
+
FAM_ScanProjects_Util.progress(globals, `=== ${specs.length} projekt SOROS scan-je${local.dryRun ? ' (dryRun)' : ''}`
|
|
87
|
+
+ `${projectCooldownMs > 0 ? `, cooldown ${projectCooldownMs}ms/projekt` : ''} ===`);
|
|
81
88
|
for (let i = 0; i < specs.length; i++) {
|
|
82
89
|
const spec = specs[i];
|
|
83
90
|
FAM_ScanProjects_Util.progress(globals, `[${i + 1}/${specs.length}] ${spec.project} (${spec.path})`);
|
|
@@ -109,6 +116,11 @@ class FAM_ScanProjects_Util {
|
|
|
109
116
|
break;
|
|
110
117
|
}
|
|
111
118
|
}
|
|
119
|
+
// Projekt-cooldown: a projektek KÖZÉ (NEM az utolsó után) — gép-tehermentesítő szünet nagy multi-scanhez.
|
|
120
|
+
if (projectCooldownMs > 0 && i < specs.length - 1) {
|
|
121
|
+
FAM_ScanProjects_Util.progress(globals, ` cooldown ${projectCooldownMs}ms…`);
|
|
122
|
+
await fam_cooldown_util_1.FAM_Cooldown_Util.sleep(projectCooldownMs);
|
|
123
|
+
}
|
|
112
124
|
}
|
|
113
125
|
FAM_ScanProjects_Util.progress(globals, `=== KÉSZ: ${okCount}/${specs.length} ok ===`);
|
|
114
126
|
return fam_output_util_1.FAM_Output_Util.success({
|
|
@@ -116,6 +128,19 @@ class FAM_ScanProjects_Util {
|
|
|
116
128
|
data: { total: specs.length, ok: okCount, projects: projects },
|
|
117
129
|
});
|
|
118
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* A projekt-cooldown ms feloldása: a CLI `--cooldown` felülírja a szerver `scan.projectCooldownMs` configját.
|
|
133
|
+
* Nincs CLI-érték → a szerver effektív configját kérdezi (`GET /api/config`); a config-lekérés hibája → 0 (a
|
|
134
|
+
* cooldown sosem buktatja a scant). Minden út a `FAM_Cooldown_Util.resolveMs`-en megy át (clamp + biztonság).
|
|
135
|
+
*/
|
|
136
|
+
static async resolveCooldownMs(client, cliValue) {
|
|
137
|
+
if (cliValue !== undefined) {
|
|
138
|
+
return fam_cooldown_util_1.FAM_Cooldown_Util.resolveMs(cliValue);
|
|
139
|
+
}
|
|
140
|
+
const result = await client.get('/api/config');
|
|
141
|
+
const value = result.ok ? result.data?.effective?.['scan.projectCooldownMs'] : 0;
|
|
142
|
+
return fam_cooldown_util_1.FAM_Cooldown_Util.resolveMs(value);
|
|
143
|
+
}
|
|
119
144
|
/** A scan-request body egy projekt-spec-hez (dryRun vagy valós). */
|
|
120
145
|
static body(spec, dryRun, defaults) {
|
|
121
146
|
return {
|
|
@@ -205,12 +205,42 @@ exports.CONFIG_CATALOG = {
|
|
|
205
205
|
type: 'boolean', default: false, levels: ALL_LEVELS,
|
|
206
206
|
description: 'Symlink-követés (path-traversal védelem, dsgn-004).',
|
|
207
207
|
},
|
|
208
|
+
'scan.nestedFamignore': {
|
|
209
|
+
type: 'boolean', default: true, levels: ALL_LEVELS,
|
|
210
|
+
description: 'A NESTED (per-alkönyvtár) `.fdpfamignore`-ok figyelembevétele a bejárás közben. `true` (default) '
|
|
211
|
+
+ '= ha egy al-projektben/al-mappában van saját `.fdpfamignore`, azt is betöltjük (a saját al-útjához '
|
|
212
|
+
+ 'horgonyozva) és alkalmazzuk arra a subtree-re — így a teljes-workspace scan tiszteletben tartja az '
|
|
213
|
+
+ 'al-projektek saját ignore-jait (eleve-tisztán-szkennelés). `false` = csak a scan-GYÖKÉR `.fdpfamignore`-ja '
|
|
214
|
+
+ 'érvényes (a régi, v1-viselkedés — ha valaki kizárólag gyökér-szintű kontrollt akar).',
|
|
215
|
+
},
|
|
216
|
+
'scan.reconcileIgnored': {
|
|
217
|
+
type: 'boolean', default: true, levels: ALL_LEVELS,
|
|
218
|
+
description: 'Re-scankor a MOST ignore-minta által kizárt (de korábban beolvasott) fájlok meglévő chunkjainak '
|
|
219
|
+
+ 'kitakarítása a tárból. `true` (default — TISZTOGATÁS) = ha utólag hozzáadsz egy ignore-/famignore-mintát, '
|
|
220
|
+
+ 'a re-scan az így kizárttá vált fájlok régi bejegyzéseit orphanként soft-delete-eli (nem ragad be, amit '
|
|
221
|
+
+ 'ignore-olni akarsz). `false` = a régi viselkedés (az ignore-skippelt fájl „jelen-lévőként" számít, a régi '
|
|
222
|
+
+ 'chunkjai BENT MARADNAK — ha valaki a korábbi beolvasást meg akarja őrizni). A lemezről ELTŰNT fájlokat '
|
|
223
|
+
+ 'mindkét beállítás reconciliálja (ez attól független).',
|
|
224
|
+
},
|
|
208
225
|
'scan.testFileWeight': {
|
|
209
226
|
type: 'number', default: 0.5, min: 0.0, max: 10.0, levels: ALL_LEVELS,
|
|
210
227
|
description: 'A teszt-/spec-fájlok (`*.spec.*` / `*.test.*`) DEFAULT retrieval-súlya (FAM-REV-058) — a próza-'
|
|
211
228
|
+ 'nehéz specek különben az IMPLEMENTÁCIÓ fölé rangsorolnak. <1 → a forrás alá; explicit scan-`weights` '
|
|
212
229
|
+ 'felülírja; 1 = nincs csökkentés.',
|
|
213
230
|
},
|
|
231
|
+
'scan.sectionCooldownMs': {
|
|
232
|
+
type: 'number', default: 0, min: 0, max: 60000, integer: true, levels: ALL_LEVELS,
|
|
233
|
+
description: 'Kikapcsolható cooldown (ms) a SZEKCIÓK (file-batch-ek) KÖZÉ egy scanen belül — a NAGY DB-k '
|
|
234
|
+
+ 'beolvasásakor ad lélegzetvételi szünetet a gépnek (CPU/embed-provider/Mongo ne terhelődjön túl). '
|
|
235
|
+
+ 'Default `0` = KIKAPCSOLVA (nincs lassítás); nagy scanre állítsd pl. 1000–3000-re. Csak a batch-ek '
|
|
236
|
+
+ 'KÖZÖTT vár (az utolsó után nem), és a `dryRun` NEM vár (nincs valódi embed-munka).',
|
|
237
|
+
},
|
|
238
|
+
'scan.projectCooldownMs': {
|
|
239
|
+
type: 'number', default: 0, min: 0, max: 60000, integer: true, levels: ALL_LEVELS,
|
|
240
|
+
description: 'Kikapcsolható cooldown (ms) a PROJEKTEK KÖZÉ a `scan-projects` soros multi-scan-jében — a sok-'
|
|
241
|
+
+ 'projektes (L2/L6) beolvasás ne thrash-elje a gépet. Default `0` = KIKAPCSOLVA; nagy futásra pl. 2000–'
|
|
242
|
+
+ '5000. A CLI `--cooldown <ms>` felülírja ezt a config-default-ot (a projektek KÖZÖTT vár, az utolsó után nem).',
|
|
243
|
+
},
|
|
214
244
|
'scan.estimateMsPerChunk': {
|
|
215
245
|
type: 'number', default: 250, min: 1, max: 10000, integer: true, levels: ALL_LEVELS,
|
|
216
246
|
description: 'A `dryRun` pre-scan idő-becsléshez: becsült embed-idő chunkonként (ms). DURVA, gép-/modell-/'
|
|
@@ -239,6 +269,32 @@ exports.CONFIG_CATALOG = {
|
|
|
239
269
|
type: 'number', default: 20, min: 1, max: 200, integer: true, levels: GLOBAL_TABLE,
|
|
240
270
|
description: 'Párhuzamos ingest flow-limit (CCAP DEFAULT_INGEST_FLOW_LIMIT=20, cap 200).',
|
|
241
271
|
},
|
|
272
|
+
// --- 4.8. Memória / heap-guard (OOM-hibakezelés, fogyasztó: FAM_HeapGuard_ControlService) ---
|
|
273
|
+
'memory.guardEnabled': {
|
|
274
|
+
type: 'boolean', default: true, levels: GLOBAL_ONLY,
|
|
275
|
+
description: 'A heap-guard (OOM-hibakezelés) be/ki. `true` (default): watchdog (heap-arány + GC-hányad) + a '
|
|
276
|
+
+ 'nehéz write-ek (scan/import/re-embed) load-shed-je critical nyomásnál + graceful-exit a near-limit '
|
|
277
|
+
+ 'zónában (a kriptikus exit-134 helyett). `false`: csak a crash-handlerek (uncaught/unhandled) aktívak.',
|
|
278
|
+
},
|
|
279
|
+
'memory.warnRatio': {
|
|
280
|
+
type: 'number', default: 0.70, min: 0.1, max: 1.0, levels: GLOBAL_ONLY,
|
|
281
|
+
description: 'Heap-arány (used/limit), ami fölött a guard `warn` szintet jelez (log; még nincs load-shed).',
|
|
282
|
+
},
|
|
283
|
+
'memory.shedRatio': {
|
|
284
|
+
type: 'number', default: 0.82, min: 0.1, max: 1.0, levels: GLOBAL_ONLY,
|
|
285
|
+
description: 'Heap-arány, ami fölött `critical` → a nehéz write-ek (scan/import/re-embed) 503-mal elutasítva '
|
|
286
|
+
+ '(a könnyű read/create/update fut tovább). A GC-hányad ≥60% is critical-t vált (mark-compact thrash-jel).',
|
|
287
|
+
},
|
|
288
|
+
'memory.exitRatio': {
|
|
289
|
+
type: 'number', default: 0.92, min: 0.1, max: 1.0, levels: GLOBAL_ONLY,
|
|
290
|
+
description: 'Heap-arány, ami fölött tartósan (GC-vel sem csökken) a guard TISZTÁN kilép (exit 137) → a `fam '
|
|
291
|
+
+ 'serve` ps1 prediktíven újraindít. A kriptikus „Ineffective mark-compacts" exit-134 elkerülése. shed < exit ajánlott.',
|
|
292
|
+
},
|
|
293
|
+
'memory.pollMs': {
|
|
294
|
+
type: 'number', default: 3000, min: 500, max: 60000, integer: true, levels: GLOBAL_ONLY,
|
|
295
|
+
description: 'A heap-watchdog mintavételi időköze (ms) + a GC-hányad gördülő-ablaka. Kisebb = gyorsabb '
|
|
296
|
+
+ 'reakció a hirtelen növekedésre, több CPU. 3000 = jó alapérték.',
|
|
297
|
+
},
|
|
242
298
|
'patterns.defaultWeight': {
|
|
243
299
|
type: 'number', default: 5, min: 0, levels: [fam_config_level_type_enum_1.FAM_ConfigLevel.table], tableOnly: fam_table_type_enum_1.FAM_Table.codingPatterns,
|
|
244
300
|
description: 'A coding_patterns tár default retrieval-súlya (CCAP weight 5; csak coding_patterns).',
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAM_ConfigValueCoerce_Util = void 0;
|
|
4
|
+
const config_catalog_const_1 = require("./config-catalog.const");
|
|
5
|
+
/**
|
|
6
|
+
* `FAM_ConfigValueCoerce_Util` — a `config set` CLI nyers `--value` STRING-jét a katalógus-kulcs típusára konvertálja
|
|
7
|
+
* (a CLI argumentum mindig string, a szerver viszont típus-szigorúan validál — boolean/number kulcsot string-ként
|
|
8
|
+
* elutasít: `FAM-CONFIG-SET-002`). Ez teszi a config-kapcsolókat (pl. `scan.nestedFamignore` / `scan.reconcileIgnored`
|
|
9
|
+
* / `scan.followSymlinks`) ténylegesen CLI-ből állíthatóvá. A `CONFIG_CATALOG[key].type` alapján:
|
|
10
|
+
*
|
|
11
|
+
* - `boolean` → `'true'`/`'false'` → bool (más → változatlan string, hogy a szerver adjon egyértelmű hibát);
|
|
12
|
+
* - `number` → `Number(raw)` (NaN → változatlan string);
|
|
13
|
+
* - `number-or-null` → `'null'`/üres → `null`, különben `Number(raw)`;
|
|
14
|
+
* - `string[]` → JSON-tömb (`[...]`) VAGY vesszős lista → `string[]`;
|
|
15
|
+
* - `string` (+ enum) → változatlan.
|
|
16
|
+
*
|
|
17
|
+
* Ismeretlen kulcs → változatlan string (a szerver `FAM-CONFIG-SET-001`-gyel utasítja el). Tisztán pure.
|
|
18
|
+
*/
|
|
19
|
+
class FAM_ConfigValueCoerce_Util {
|
|
20
|
+
/** A nyers CLI-string → a `key` katalógus-típusának megfelelő JS-érték (lásd az osztály-leírást). */
|
|
21
|
+
static coerce(key, raw) {
|
|
22
|
+
const entry = config_catalog_const_1.CONFIG_CATALOG[key];
|
|
23
|
+
if (!entry) {
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
switch (entry.type) {
|
|
27
|
+
case 'boolean':
|
|
28
|
+
if (raw === 'true') {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (raw === 'false') {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return raw;
|
|
35
|
+
case 'number': {
|
|
36
|
+
const value = Number(raw);
|
|
37
|
+
return Number.isNaN(value) ? raw : value;
|
|
38
|
+
}
|
|
39
|
+
case 'number-or-null': {
|
|
40
|
+
if (raw === 'null' || raw === '') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const value = Number(raw);
|
|
44
|
+
return Number.isNaN(value) ? raw : value;
|
|
45
|
+
}
|
|
46
|
+
case 'string[]': {
|
|
47
|
+
const trimmed = raw.trim();
|
|
48
|
+
if (trimmed.startsWith('[')) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(trimmed);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// nem valid JSON → essünk át a vesszős listára
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return trimmed.length ? trimmed.split(',').map((item) => item.trim()).filter(Boolean) : [];
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.FAM_ConfigValueCoerce_Util = FAM_ConfigValueCoerce_Util;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAM_Cooldown_Util = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `FAM_Cooldown_Util` (user-FR 2026-06-21) — opcionális, **kikapcsolható** cooldown (lélegzetvételi szünet) a
|
|
6
|
+
* scan-feldolgozás közé, hogy a NAGY adatbázisok beolvasása NE terhelje túl a gépet (CPU/embed-provider/Mongo).
|
|
7
|
+
* Két ponton fogyasztva: (a) a SZEKCIÓK (file-batch-ek) között egy scanen belül (`scan.sectionCooldownMs`, szerver-
|
|
8
|
+
* oldali ingest-loop); (b) a PROJEKTEK között a `scan-projects` soros orchestrátorban (`scan.projectCooldownMs` /
|
|
9
|
+
* CLI `--cooldown`). Default **0 = kikapcsolva** (nincs lassítás) → opt-in: a user nagy scanre bekapcsolja.
|
|
10
|
+
*
|
|
11
|
+
* Tisztán: a `resolveMs` (parse + clamp) pure + külön tesztelhető; a `sleep` az egyetlen side-effect.
|
|
12
|
+
*/
|
|
13
|
+
class FAM_Cooldown_Util {
|
|
14
|
+
/** Biztonsági felső korlát egy cooldown-ra (ms) — egy elgépelt óriás-érték ne fagyassza be a scant. */
|
|
15
|
+
static MAX_MS = 60000;
|
|
16
|
+
/**
|
|
17
|
+
* Nyers config/CLI érték → biztonságos cooldown ms. Nem-szám / NaN / ≤0 → `0` (kikapcsolva); egyébként egész
|
|
18
|
+
* ms-re vágva, `[0, MAX_MS]`-re clamp-elve. Így a hibás/extrém input sosem buktatja vagy fagyasztja a scant.
|
|
19
|
+
*/
|
|
20
|
+
static resolveMs(raw) {
|
|
21
|
+
const value = typeof raw === 'number' ? raw : Number(raw);
|
|
22
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
return Math.min(Math.floor(value), FAM_Cooldown_Util.MAX_MS);
|
|
26
|
+
}
|
|
27
|
+
/** Vár `ms` ideig (≤0 → azonnal visszatér, nincs timer). A cooldown egyetlen tényleges késleltetése. */
|
|
28
|
+
static async sleep(ms) {
|
|
29
|
+
if (ms <= 0) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.FAM_Cooldown_Util = FAM_Cooldown_Util;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAM_HeapGuard_ControlService = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const v8 = tslib_1.__importStar(require("v8"));
|
|
6
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
7
|
+
const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
|
|
8
|
+
/**
|
|
9
|
+
* `FAM_HeapGuard_ControlService` (user-FR 2026-06-21) — **OOM-hibakezelés**. A FAM-szerver V8-heapje a nagy korpuszon
|
|
10
|
+
* (126k vektor) + nehéz műveletekkor (scan/import/re-embed) a plafonhoz érhet → „Ineffective mark-compacts near heap
|
|
11
|
+
* limit" → FATAL exit 134 (nem catch-elhető). Ez a guard:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Watchdog**: periodikusan méri a heap-arányt (`v8.getHeapStatistics`) ÉS a GC-hányadot (`perf_hooks` 'gc'
|
|
14
|
+
* observer — a thrash valódi jele: a trace-ben `mu = 0.306`, azaz a futás 70%-a GC). A szint-váltásokat logolja
|
|
15
|
+
* (a `/api/logs`-on az agent IS látja jönni a nyomást).
|
|
16
|
+
* 2. **Load-shed**: `shouldShedLoad()` → a NEHÉZ write-ek (scan/import/re-embed) `critical` nyomásnál 503-mal
|
|
17
|
+
* elutasítva, MIELŐTT egy újabb nagy allokáció átlöki a plafont (a könnyű read/create/update fut tovább).
|
|
18
|
+
* 3. **Graceful exit**: ha a nyomás a `exitRatio` fölött tartós (GC-vel sem csökken), strukturált diagnosztika +
|
|
19
|
+
* tiszta `process.exit` → a `fam serve` ps1-wrapper PREDIKTÍVAN újraindít (a kriptikus exit-134 helyett).
|
|
20
|
+
* 4. **Crash-handler**: `uncaughtException` / `unhandledRejection` → strukturált log + tiszta exit (eddig néma volt).
|
|
21
|
+
*
|
|
22
|
+
* Lokális, single-user (ADR-004) → a load-shed a saját gépet védi, nem multi-tenant fairness. A küszöbök
|
|
23
|
+
* config-/env-kapcsolósak; a tiszta szint-számítás (`computeLevel`) pure + külön tesztelt.
|
|
24
|
+
*/
|
|
25
|
+
class FAM_HeapGuard_ControlService {
|
|
26
|
+
static _instance;
|
|
27
|
+
/** GC-hányad-küszöb a `warn`/`critical` szinthez (a thrash-jelhez; a ratio MELLETT). */
|
|
28
|
+
static GC_WARN_FRACTION = 0.40;
|
|
29
|
+
static GC_CRITICAL_FRACTION = 0.60;
|
|
30
|
+
thresholds = {
|
|
31
|
+
warnRatio: 0.70, shedRatio: 0.82, exitRatio: 0.92, pollMs: 3000, enabled: true,
|
|
32
|
+
};
|
|
33
|
+
installed = false;
|
|
34
|
+
timer;
|
|
35
|
+
gcObserver;
|
|
36
|
+
lastLevel = 'ok';
|
|
37
|
+
/** A GC-ben töltött ms gördülő ablaka (timestamp + duration párok az utolsó ~pollMs-re). */
|
|
38
|
+
gcEvents = [];
|
|
39
|
+
static getInstance() {
|
|
40
|
+
if (!FAM_HeapGuard_ControlService._instance) {
|
|
41
|
+
FAM_HeapGuard_ControlService._instance = new FAM_HeapGuard_ControlService();
|
|
42
|
+
}
|
|
43
|
+
return FAM_HeapGuard_ControlService._instance;
|
|
44
|
+
}
|
|
45
|
+
/** A küszöbök beállítása (install ELŐTT) — clamp `[0..1]` (a pollMs `[500..60000]`). Hiányzó mező → marad a default. */
|
|
46
|
+
configure(partial) {
|
|
47
|
+
const clampRatio = (value, fallback) => (typeof value === 'number' && value > 0 && value <= 1) ? value : fallback;
|
|
48
|
+
this.thresholds = {
|
|
49
|
+
warnRatio: clampRatio(partial.warnRatio, this.thresholds.warnRatio),
|
|
50
|
+
shedRatio: clampRatio(partial.shedRatio, this.thresholds.shedRatio),
|
|
51
|
+
exitRatio: clampRatio(partial.exitRatio, this.thresholds.exitRatio),
|
|
52
|
+
pollMs: typeof partial.pollMs === 'number' && partial.pollMs >= 500
|
|
53
|
+
? Math.min(partial.pollMs, 60000) : this.thresholds.pollMs,
|
|
54
|
+
enabled: typeof partial.enabled === 'boolean' ? partial.enabled : this.thresholds.enabled,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** A crash-handlerek + (ha enabled) a watchdog + GC-observer telepítése. Idempotens (egyszer fut). Best-effort. */
|
|
58
|
+
install() {
|
|
59
|
+
if (this.installed) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.installed = true;
|
|
63
|
+
process.on('uncaughtException', (error) => this.onFatal('uncaughtException', error));
|
|
64
|
+
process.on('unhandledRejection', (reason) => this.onFatal('unhandledRejection', reason));
|
|
65
|
+
if (!this.thresholds.enabled) {
|
|
66
|
+
fsm_dynamo_1.DyFM_Log.info('[heap-guard] disabled (memory.guardEnabled=false) — csak a crash-handlerek aktívak.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.installGcObserver();
|
|
70
|
+
this.timer = setInterval(() => this.tick(), this.thresholds.pollMs);
|
|
71
|
+
// A timer NE tartsa életben a process-t (a graceful shutdownt ne blokkolja).
|
|
72
|
+
this.timer.unref?.();
|
|
73
|
+
fsm_dynamo_1.DyFM_Log.info(`[heap-guard] aktív (poll ${this.thresholds.pollMs}ms; shed@${Math.round(this.thresholds.shedRatio * 100)}%, `
|
|
74
|
+
+ `exit@${Math.round(this.thresholds.exitRatio * 100)}%).`);
|
|
75
|
+
}
|
|
76
|
+
/** Pure szint-számítás (külön tesztelhető): a ratio ÉS a GC-hányad közül a ROSSZABB dönt. */
|
|
77
|
+
static computeLevel(ratio, gcFraction, thresholds) {
|
|
78
|
+
if (ratio >= thresholds.shedRatio || gcFraction >= FAM_HeapGuard_ControlService.GC_CRITICAL_FRACTION) {
|
|
79
|
+
return 'critical';
|
|
80
|
+
}
|
|
81
|
+
if (ratio >= thresholds.warnRatio || gcFraction >= FAM_HeapGuard_ControlService.GC_WARN_FRACTION) {
|
|
82
|
+
return 'warn';
|
|
83
|
+
}
|
|
84
|
+
return 'ok';
|
|
85
|
+
}
|
|
86
|
+
/** A pillanatnyi heap-nyomás (ratio + GC-hányad → szint). Observability + load-shed forrása. */
|
|
87
|
+
pressure() {
|
|
88
|
+
const stats = v8.getHeapStatistics();
|
|
89
|
+
const usedMb = Math.round(stats.used_heap_size / 1048576);
|
|
90
|
+
const limitMb = Math.round(stats.heap_size_limit / 1048576);
|
|
91
|
+
const ratio = stats.heap_size_limit ? stats.used_heap_size / stats.heap_size_limit : 0;
|
|
92
|
+
const gcFraction = this.recentGcFraction();
|
|
93
|
+
const level = FAM_HeapGuard_ControlService.computeLevel(ratio, gcFraction, this.thresholds);
|
|
94
|
+
return { level: level, usedMb: usedMb, limitMb: limitMb, ratio: ratio, gcFraction: gcFraction };
|
|
95
|
+
}
|
|
96
|
+
/** `true` → a NEHÉZ write-eket (scan/import/re-embed) le kell shed-elni (critical nyomás). Disabled → soha. */
|
|
97
|
+
shouldShedLoad() {
|
|
98
|
+
return this.thresholds.enabled && this.pressure().level === 'critical';
|
|
99
|
+
}
|
|
100
|
+
/** A GC-hányad az utolsó `pollMs` ablakban (GC-ms / ablak-ms; clamp [0..1]). */
|
|
101
|
+
recentGcFraction() {
|
|
102
|
+
const windowMs = this.thresholds.pollMs;
|
|
103
|
+
const cutoff = Date.now() - windowMs;
|
|
104
|
+
this.gcEvents = this.gcEvents.filter((event) => event.at >= cutoff);
|
|
105
|
+
const gcMs = this.gcEvents.reduce((sum, event) => sum + event.ms, 0);
|
|
106
|
+
return Math.min(gcMs / windowMs, 1);
|
|
107
|
+
}
|
|
108
|
+
/** A 'gc' perf-entry-k figyelése — a GC-ben töltött időt gyűjti a gördülő ablakhoz. */
|
|
109
|
+
installGcObserver() {
|
|
110
|
+
try {
|
|
111
|
+
this.gcObserver = new perf_hooks_1.PerformanceObserver((list) => {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
for (const entry of list.getEntries()) {
|
|
114
|
+
this.gcEvents.push({ at: now, ms: entry.duration });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
this.gcObserver.observe({ entryTypes: ['gc'] });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// A GC-observer nem kritikus (a ratio-jel marad) — best-effort.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** A watchdog-tick: szint-váltás logolása + a tartós veszély-zóna graceful-exit-je. */
|
|
124
|
+
tick() {
|
|
125
|
+
const pressure = this.pressure();
|
|
126
|
+
if (pressure.level !== this.lastLevel) {
|
|
127
|
+
const line = `[heap-guard] ${this.lastLevel}→${pressure.level} `
|
|
128
|
+
+ `(${pressure.usedMb}/${pressure.limitMb} MB ${Math.round(pressure.ratio * 100)}%, GC ${Math.round(pressure.gcFraction * 100)}%)`;
|
|
129
|
+
if (pressure.level === 'critical') {
|
|
130
|
+
fsm_dynamo_1.DyFM_Log.warn(`${line} — a nehéz write-ek (scan/import/re-embed) load-shed (503).`);
|
|
131
|
+
}
|
|
132
|
+
else if (pressure.level === 'warn') {
|
|
133
|
+
fsm_dynamo_1.DyFM_Log.warn(line);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
fsm_dynamo_1.DyFM_Log.info(`${line} — nyomás enyhült.`);
|
|
137
|
+
}
|
|
138
|
+
this.lastLevel = pressure.level;
|
|
139
|
+
}
|
|
140
|
+
// Tartós veszély-zóna: GC-kísérlet, majd ha nem csökken → tiszta, logolt exit (a ps1 újraindít).
|
|
141
|
+
if (pressure.ratio >= this.thresholds.exitRatio) {
|
|
142
|
+
this.attemptGcReclaim();
|
|
143
|
+
const after = this.pressure();
|
|
144
|
+
if (after.ratio >= this.thresholds.exitRatio) {
|
|
145
|
+
fsm_dynamo_1.DyFM_Log.error(`[heap-guard] HEAP-VÉSZ ${after.usedMb}/${after.limitMb} MB (${Math.round(after.ratio * 100)}%) — `
|
|
146
|
+
+ 'graceful exit, hogy a `fam serve` TISZTÁN újraindítson (a kriptikus OOM exit-134 elkerülése).');
|
|
147
|
+
// Egy tick haladékot adunk a log flush-nek, majd restartolható kóddal kilépünk.
|
|
148
|
+
setTimeout(() => process.exit(137), 250).unref?.();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Best-effort GC-kényszerítés (csak ha `--expose-gc`). A near-limit utolsó esély a tiszta-exit ELŐTT. */
|
|
153
|
+
attemptGcReclaim() {
|
|
154
|
+
try {
|
|
155
|
+
const maybeGc = global.gc;
|
|
156
|
+
if (maybeGc) {
|
|
157
|
+
maybeGc();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// nincs --expose-gc → nincs mit tenni; a tiszta-exit a fallback.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Strukturált crash-log + tiszta exit (a `fam serve` ps1 újraindít). Az OOM-fatal NEM ide jön (nem catch-elhető). */
|
|
165
|
+
onFatal(kind, error) {
|
|
166
|
+
try {
|
|
167
|
+
const message = error instanceof Error ? `${error.message}\n${error.stack ?? ''}` : String(error);
|
|
168
|
+
fsm_dynamo_1.DyFM_Log.error(`[heap-guard] FATAL ${kind}: ${message}`);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// a log se buktasson — best-effort.
|
|
172
|
+
}
|
|
173
|
+
setTimeout(() => process.exit(1), 250).unref?.();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.FAM_HeapGuard_ControlService = FAM_HeapGuard_ControlService;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAM_title = void 0;
|
|
4
|
+
exports.FAM_title = `
|
|
5
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠤⠤⠤⠤⣄⠀⠀⠀⠀⠀⠀⢀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
6
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣶⡊⠉⠉⣉⣱⡷⠶⢢⣠⢴⣶⡝⠒⠉⢉⣭⡽⠟⢉⣀⡀⠹⢭⠒⢤⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
7
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠔⢚⣩⡽⠿⠊⢉⣉⡂⣀⣩⠭⢴⠟⠋⠉⠉⠉⠛⠳⢦⣬⣤⡴⠞⠛⠁⠛⠳⣾⣧⠀⠟⠀⠉⠲⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
8
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⢚⠁⠀⠰⠋⢡⠄⠀⠞⣫⢟⡥⠒⠉⠹⣿⡀⠀⠀⢦⡀⠀⠀⠀⠈⠻⡧⡀⠀⠀⠀⠀⠈⠻⣗⡶⠶⠶⢤⡀⠱⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
9
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢮⣤⡾⠀⠀⣠⡴⠋⠀⡠⣚⠥⠒⢛⡲⠄⠀⠈⢻⡆⠀⠀⠻⣦⣀⠀⠀⠀⣿⠻⣦⣀⣴⠶⠂⠀⠘⣷⡄⠀⠀⢀⣴⡿⠈⠢⡀⠀⠀⠀⠀⠀⠀
|
|
10
|
+
⠀⠀⠀⠀⠀⠀⠀⡠⠖⣉⣁⡀⠀⢀⣾⠋⠴⢿⣽⠋⠀⠞⢉⣉⣽⣳⣄⣀⠀⠋⠀⠀⠀⠈⠙⣷⡄⠀⠁⠀⠙⢤⣯⡀⠀⠀⠀⣼⡇⠀⡾⠋⠁⠀⠳⣄⠘⢆⠀⠀⠀⠀⠀
|
|
11
|
+
⠀⠀⠀⠀⠀⡰⠋⠰⠛⢻⡞⢉⣠⣼⡇⢀⣴⠟⠛⠒⣴⠟⠋⠉⠀⠀⢀⣀⣀⡀⠀⠀⠀⠀⠀⣸⡇⠀⠀⠳⣄⠀⠉⢿⣄⠀⢰⣿⣧⡀⠀⣴⠶⠶⣦⡼⢧⠈⢣⠀⠀⠀⠀
|
|
12
|
+
⠀⠀⠀⢀⡞⣡⣶⡄⠀⡟⣳⠿⠋⠙⡍⡽⠁⣀⣤⣤⣿⡄⠀⠀⠀⠀⡿⡉⣀⣀⣤⣤⣤⣴⠾⠥⠽⣦⣄⠀⠉⠻⢶⡼⢻⠀⠈⠇⠘⡷⡄⠘⠂⠀⢀⡍⠻⣷⣄⡇⠀⠀⠀
|
|
13
|
+
⠀⠀⠀⠘⣺⠏⢸⢃⣼⠟⢁⡤⠀⣠⢟⡷⠟⢋⣉⣤⡿⠇⠀⠀⠀⢰⣣⠞⠋⠉⠉⠁⡀⠀⠀⠀⠀⠀⠙⢷⣄⠀⠀⢹⣾⠀⠀⠀⠀⢸⡇⠀⣀⡀⣾⠀⠀⠈⢻⡁⢦⠀⠀
|
|
14
|
+
⠀⠀⣠⢚⣵⣄⠈⣼⡇⠀⢸⠧⢞⡵⠋⠠⠚⠉⠉⠀⠀⢀⡇⠀⣰⣟⣁⣀⠀⠀⠀⠀⠉⠒⠶⣤⣤⣀⠀⠀⠙⠀⠀⢸⡇⢰⣟⠛⢶⡋⣇⠀⠉⠻⡟⡄⠀⢀⢀⣿⠀⢧⠀
|
|
15
|
+
⠀⣰⠃⢸⠁⣿⠀⠸⣧⠀⢸⢣⠋⠀⣠⣤⠶⢶⢒⣤⣔⣻⠣⢼⠟⠁⠀⠙⢷⡄⠀⠀⢀⠀⠀⠀⠈⠓⢟⢦⠀⠀⠀⢸⡇⠈⠻⣦⡀⠈⠻⣷⣄⠀⠘⣿⠀⠸⣿⣇⣀⢸⠀
|
|
16
|
+
⠀⡇⠀⠀⣼⠇⠀⢀⣿⠀⣇⣇⣴⠟⠋⢠⣾⠟⠉⠀⠀⠈⠳⣼⠀⠀⠀⠀⠀⠳⠀⠀⠈⢳⣄⠀⠀⠀⢸⣼⠀⠀⠀⠈⡟⢆⠀⠈⢻⡀⠀⠈⢻⣆⠀⣻⠃⠀⠀⢹⡟⠻⡀
|
|
17
|
+
⠀⢧⡆⣼⠏⠀⣾⠟⠁⢰⠃⡵⠃⢀⣴⡿⠁⠀⡀⠀⠀⠀⠀⠹⣧⡀⠰⣦⡀⠀⠀⠀⠀⠀⣻⢦⣀⣠⡾⣇⠀⠀⢀⣰⠟⠙⢷⣄⠀⠀⠀⠀⠀⣿⠀⠉⢠⠄⠀⣼⡇⠀⢧
|
|
18
|
+
⢀⠞⢡⡟⠀⠀⣿⠀⢀⡏⡼⠁⣴⠟⠁⠀⠀⠀⣿⠀⣀⣀⢀⣴⠘⣷⡀⠈⢻⣦⣀⠀⢀⣾⠟⠉⠀⠀⠉⠻⣷⣄⠀⠀⠀⠀⠀⠙⢷⡄⠀⠀⠀⠉⠀⣠⡟⠀⣼⣟⠀⠀⢸
|
|
19
|
+
⢸⠀⠘⣧⠀⡴⠛⠳⢸⢰⠁⢰⠏⠀⠀⠀⢀⣼⡯⠟⠋⠙⠻⣷⡀⠘⠀⠀⠀⠈⠉⠻⣿⠁⠀⠀⢰⡟⠉⠀⠈⢻⣦⠀⠀⠀⣄⠀⠀⡗⠀⢸⡇⣠⣾⠟⢀⣾⠋⠹⣷⢀⡇
|
|
20
|
+
⠈⢆⠀⠹⢷⣤⣀⣠⠎⡇⠀⠸⠀⠀⢀⣴⠟⠉⠀⠀⠀⢄⠀⠹⣧⡀⠀⠀⣀⡀⠀⠀⣿⠀⠀⠀⠘⣿⡄⠀⠀⠀⢹⣦⡀⠀⢿⣄⠀⢀⣠⡿⠽⣯⡁⠀⠸⠃⠀⠀⡏⠉⠀
|
|
21
|
+
⠀⢠⢷⣄⠀⠈⣉⣉⢢⢳⡀⠀⠀⠀⣾⡏⠀⠀⠠⣀⡤⢿⠀⠀⠙⠷⠶⠛⠉⠈⠀⣰⠟⠀⠀⠀⠀⠘⣷⡀⠀⠠⠛⠉⠉⠀⢈⣯⠗⠛⠁⠀⠀⠈⠃⠀⢀⣴⠇⢠⠇⠀⠀
|
|
22
|
+
⠀⢸⡀⠻⣧⠈⠉⠹⣏⢀⣑⠤⣀⣀⠼⠳⣄⠀⠀⠀⠙⠺⠖⣦⣤⠤⣀⡀⠀⠀⠘⠁⠀⠀⠀⠀⠀⠀⢸⢧⡀⠀⠀⢀⣀⢴⣿⣅⡀⠠⠶⢿⢦⣀⣠⣴⠟⠃⡠⠋⠀⠀⠀
|
|
23
|
+
⠀⠀⠳⡀⠘⠃⠀⡤⠸⣼⠀⠉⠛⠋⠉⠉⠙⠻⣦⣄⠀⠀⠀⠀⠈⠉⠙⠻⣦⠀⠀⠀⠀⡀⠀⠀⠀⢀⣾⠖⠚⠛⠛⠛⠋⠁⠀⠙⣷⠀⠀⣸⡴⠛⠉⢁⡤⠊⠁⠀⠀⠀⠀
|
|
24
|
+
⠀⠀⠀⠘⢦⡀⠸⣧⠀⢻⢇⠀⠳⡤⣤⠆⠀⠀⠈⢻⡇⠀⠀⠀⢰⡄⠀⠀⣿⡇⢀⡾⠛⠛⠻⡝⣲⠟⠋⠀⢀⡄⠀⠀⠀⣀⡄⠀⠋⢀⡴⣻⡄⣤⡶⡍⠀⠀⠀⠀⠀⠀⠀
|
|
25
|
+
⠀⠀⠀⠀⠀⠈⠙⠁⠉⠉⠈⠣⡀⠹⣇⠀⠀⠀⠀⠘⠀⠀⠀⢀⡾⢳⡶⠾⠋⠀⠈⠃⠀⠀⣠⠟⢄⣀⣠⡴⠋⠀⠀⠀⣼⢻⣤⣴⠶⠟⠋⣡⡷⣏⢿⡧⠀⠀⠀⠀⠀⠀⠀
|
|
26
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⢫⣲⣤⣀⣀⣀⣀⣤⣶⣻⠋⠛⠷⣦⣤⣤⣄⡤⢤⣺⠕⠋⠉⠉⠁⠀⠀⣀⣤⣾⠏⢩⠀⠀⢀⣤⣾⠛⣧⢻⣼⠀⠀⠀⠀⠀⠀⠀⠀
|
|
27
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⠮⢭⣉⣉⡩⠥⠚⠈⢇⠀⢠⡄⠀⠉⠉⠙⣿⠀⢠⠶⠖⢫⣩⠟⠛⠛⠉⠀⣠⣿⣦⠶⠿⣭⣸⣇⡿⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀
|
|
28
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⣌⡿⣄⠀⠒⠚⠋⠀⠀⠀⣠⡾⠃⠀⢀⣀⠴⠚⠉⠣⢍⣛⣶⡶⠝⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
29
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠒⠂⠀⠒⠒⠉⠀⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
30
|
+
|
|
31
|
+
FDP Agent Memory - By Future Development Program Kft.`;
|
|
@@ -4,6 +4,7 @@ exports.FAM_FamIgnore_Util = void 0;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const fam_glob_match_util_1 = require("./fam-glob-match.util");
|
|
7
8
|
/**
|
|
8
9
|
* `FAM_FamIgnore_Util` (descriptive user feedback / nagy-scan ergonómia) — a **`.fdpfamignore`** in-repo,
|
|
9
10
|
* gitignore-stílusú ignore-fájl támogatása. A projekt (vagy a workspace-gyökér) MAGÁBAN hordozza, mit hagyjon
|
|
@@ -13,7 +14,10 @@ const path = tslib_1.__importStar(require("path"));
|
|
|
13
14
|
* **Szintaxis (gitignore-szerű, a `FAM_GlobMatch_Util`-ra fordítva):** soronként egy minta; `#` komment + üres
|
|
14
15
|
* sor kihagyva. `node_modules` (csupasz név) → bárhol illeszkedik; `/build` (vezető `/`) → a gyökérhez horgonyzott;
|
|
15
16
|
* `dist/` (záró `/`) → könyvtár; `src/gen` (beágyazott `/`) → gyökér-relatív; `*.tgz` → bárhol. **A `!` negáció
|
|
16
|
-
*
|
|
17
|
+
* (re-include) TÁMOGATOTT** (user-FR 2026-06-21): `parse` az ignore-sorokat, `parseNegations` a `!`-sorokat adja;
|
|
18
|
+
* a fogyasztó (scan) a fájlt akkor SKIP-eli, ha ignore-match ÉS NEM re-include-match — a könyvtár-prune pedig
|
|
19
|
+
* `dirOnReincludeLineage`-tudatos (egy ignore-olt mappát NEM vág le, ha re-included elem van rajta/alatta). Így
|
|
20
|
+
* a „ignore-mindent-kivéve-X" (pl. `ccap/` + `!ccap/client/`) helyesen működik a rekurzív bejárásban is.
|
|
17
21
|
*
|
|
18
22
|
* **v1 hatókör:** a scan-GYÖKÉR `.fdpfamignore`-ja (a teljes scanre). A NESTED (per-alkönyvtár) `.fdpfamignore`
|
|
19
23
|
* a két-fázisú felderítés (walk → classify) miatt külön kör — BACKLOG (a `baseDir` paraméter már készen áll rá).
|
|
@@ -62,9 +66,61 @@ class FAM_FamIgnore_Util {
|
|
|
62
66
|
}
|
|
63
67
|
return patterns;
|
|
64
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* A `!`-negáció (**re-include**) sorai — a vezető `!` levágva. Tisztán pure (a fordítás külön). Az `ignore`
|
|
71
|
+
* (`parse`) + a re-include (`parseNegations`) együtt adja a gitignore-szerű „ignore-mindent-kivéve" szemantikát.
|
|
72
|
+
*/
|
|
73
|
+
static parseNegations(content) {
|
|
74
|
+
const result = [];
|
|
75
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
76
|
+
const line = rawLine.trim();
|
|
77
|
+
if (line.startsWith('!') && line.length > 1) {
|
|
78
|
+
result.push(line.slice(1).trim());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A re-include (`!`) szabályok egy könyvtárból: **`globs`** (a fájl-szintű re-include-matchhez, `<base>` +
|
|
85
|
+
* `<base>/**`) + **`bases`** (a `/**` nélküli alap-utak — a KÖNYVTÁR-prune lineage-ellenőrzéséhez: egy ignore-olt
|
|
86
|
+
* könyvtárat NEM vágunk le, ha egy re-include alatta/fölötte/rajta van). Hiányzó fájl → üres. Fail-safe.
|
|
87
|
+
*/
|
|
88
|
+
static loadReincludeRules(dirAbsolutePath, baseDir = '') {
|
|
89
|
+
try {
|
|
90
|
+
const filePath = path.join(dirAbsolutePath, FAM_FamIgnore_Util.FILENAME);
|
|
91
|
+
if (!fs.existsSync(filePath)) {
|
|
92
|
+
return { globs: [], bases: [] };
|
|
93
|
+
}
|
|
94
|
+
const negations = FAM_FamIgnore_Util.parseNegations(fs.readFileSync(filePath, 'utf-8'));
|
|
95
|
+
const globs = FAM_FamIgnore_Util.toGlobPatterns(negations, baseDir);
|
|
96
|
+
const bases = [...new Set(globs.map((glob) => glob.replace(/\/\*\*$/, '')))];
|
|
97
|
+
return { globs: globs, bases: bases };
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { globs: [], bases: [] };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `true`, ha a `dir` (root-relatív út) NEM prune-olható egy re-include miatt: (1) maga a dir re-included (vagy
|
|
105
|
+
* egy re-included alfa alatt van) — ezt **glob-match**-csel ellenőrizzük a re-include-`globs` ellen (így a
|
|
106
|
+
* `(glob)/client` minta a `client` dir-re, a `(glob)/client/(glob)` a `client/src` dir-re is illeszkedik); VAGY
|
|
107
|
+
* (2) a dir egy **anchored** (glob-mentes) re-include base ŐSE (van alatta re-included elem) — ezt string-prefixszel.
|
|
108
|
+
* (A bare-bázisok ős-esetét az (1) fedi.) Ez teszi a negációt traversal-helyessé (a szülőt nem vágjuk le idő előtt).
|
|
109
|
+
*/
|
|
110
|
+
static dirOnReincludeLineage(dir, reincludeGlobs, reincludeBases) {
|
|
111
|
+
const d = dir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
112
|
+
if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(d, reincludeGlobs)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return reincludeBases.some((rawBase) => {
|
|
116
|
+
const base = rawBase.replace(/\/+$/, '');
|
|
117
|
+
return !base.includes('*') && base.startsWith(`${d}/`);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
65
120
|
/**
|
|
66
121
|
* A `.fdpfamignore` beolvasása + fordítása egy könyvtárból (a `dirAbsolutePath`-ban). Hiányzó / olvashatatlan
|
|
67
122
|
* fájl → `[]` (opt-in: csak ha létezik). A `baseDir` a root-relatív horgony (a gyökér-szintű hívásnál `''`).
|
|
123
|
+
* (Csak az IGNORE minták — a re-include-ot a `loadReincludeRules` adja.)
|
|
68
124
|
*/
|
|
69
125
|
static loadFromDir(dirAbsolutePath, baseDir = '') {
|
|
70
126
|
try {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAM_ScanReconcile_Util = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `FAM_ScanReconcile_Util` (user-FR 2026-06-21) — a re-scan **whole-file orphan reconciliation** present-set
|
|
6
|
+
* számítása. A „present" halmaz az a fájl-útvonal-készlet, amit a friss scan a lemezen MEGTALÁLT (és ezért a
|
|
7
|
+
* táron meglévő, e halmazon KÍVÜLI `scan`-entry-k orphanok → soft-delete). A finomság a **most ignore-mintával
|
|
8
|
+
* kizárt** fájlok kezelése (`scan.reconcileIgnored`):
|
|
9
|
+
*
|
|
10
|
+
* - **`reconcileIgnored = true` (default — TISZTOGATÁS):** az ignore-minta által skippelt fájl NEM számít
|
|
11
|
+
* present-nek → a korábban beolvasott, de most ignore-olttá vált fájl régi chunkjai orphanként kitakaríthatók
|
|
12
|
+
* (ne ragadjon be, amit ignore-olni akarunk). A többi skip-ok (secret / méret / unrouted / include-filter) MARAD
|
|
13
|
+
* present (a fájl a lemezen van, csak most nem ingesteljük — NEM töröljük a régi tudást).
|
|
14
|
+
* - **`reconcileIgnored = false` (régi viselkedés):** MINDEN skippelt fájl present → a régi chunkok megmaradnak.
|
|
15
|
+
*
|
|
16
|
+
* A lemezről ELTŰNT fájlok mindkét módban orphanok (eleve nincsenek a routed/skipped halmazban) — ez a beállítástól
|
|
17
|
+
* független. Tisztán pure (Set-számítás), külön tesztelhető; a `reconcileDeletedFiles` ezt hívja.
|
|
18
|
+
*/
|
|
19
|
+
class FAM_ScanReconcile_Util {
|
|
20
|
+
/**
|
|
21
|
+
* A friss scan present-path halmaza (abszolút utak). A `routedFiles` mind present; a `skippedFiles` present,
|
|
22
|
+
* KIVÉVE az ignore-minta-kizártakat (`excludedByIgnore`), ha `reconcileIgnored` — azok orphanná válnak, hogy a
|
|
23
|
+
* régi chunkjaik kitakaríthatók legyenek. Üres input → üres halmaz.
|
|
24
|
+
*/
|
|
25
|
+
static computePresentPaths(routedFiles, skippedFiles, reconcileIgnored) {
|
|
26
|
+
const present = new Set();
|
|
27
|
+
for (const routed of routedFiles) {
|
|
28
|
+
present.add(routed.absolutePath);
|
|
29
|
+
}
|
|
30
|
+
for (const skipped of skippedFiles) {
|
|
31
|
+
// A most ignore-mintával kizárt fájl NEM present (ha tisztogatunk) → a régi bejegyzése orphan → törölhető.
|
|
32
|
+
if (reconcileIgnored && skipped.excludedByIgnore) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
present.add(skipped.absolutePath);
|
|
36
|
+
}
|
|
37
|
+
return present;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.FAM_ScanReconcile_Util = FAM_ScanReconcile_Util;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAM_SecretContent_Util = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `FAM_SecretContent_Util` (user-FR 2026-06-21) — **tartalmi** (fájlon-BELÜLi) secret-redakció. A `FAM_SecretExclude_Util`
|
|
6
|
+
* csak fájlNÉV-alapú (a `.env`/`*.key` típusú titok-FÁJLOKAT zárja ki); de egy `.ts`/`.py` FORRÁSBA hardcode-olt
|
|
7
|
+
* API-kulcs (pl. `apiKey: 'sk-…'`) eddig beolvasódott a vektor-tárba (élő lelet 2026-06-21: 1 OpenAI-kulcs 4 config-
|
|
8
|
+
* fájlban). Ez a util a fájl-tartalmat OLVASÁSKOR redaktálja (a chunk/contentHash/embedding/tárolt szöveg MIND a
|
|
9
|
+
* redaktált változatot kapja) → a titok soha nem kerül a DB-be, és egy korábban-szivárgott fájl RE-SCANKOR
|
|
10
|
+
* auto-gyógyul (a contentHash változik a redakciótól → `modified` → a tárolt content felülíródik).
|
|
11
|
+
*
|
|
12
|
+
* **Magas-konfidenciájú, jellegzetes alakú token-minták** (alacsony false-positive): OpenAI `sk-…`, AWS `AKIA…`,
|
|
13
|
+
* GitHub `ghp_/gho_…`, Google `AIza…`, Slack `xox.-…`, privát-kulcs PEM-blokk, JWT. NEM próbál generikus
|
|
14
|
+
* `password=…`-t fogni (az túl sok forrás-konstanst redaktálna) — a cél a VALÓDI kulcs-szivárgás, nem a kód-RAG rontása.
|
|
15
|
+
*/
|
|
16
|
+
class FAM_SecretContent_Util {
|
|
17
|
+
/** A redaktált tokenek helyére kerülő jelölő (a kód-kontextus megmarad, csak a titok tűnik el). */
|
|
18
|
+
static PLACEHOLDER = '***REDACTED-SECRET***';
|
|
19
|
+
/** Magas-konfidenciájú secret-token minták (jellegzetes prefix + hossz → kevés false-positive). */
|
|
20
|
+
static PATTERNS = [
|
|
21
|
+
{ name: 'openai', re: /sk-(?:proj-)?[A-Za-z0-9_-]{32,}/g },
|
|
22
|
+
{ name: 'aws-access-key', re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
23
|
+
{ name: 'github-token', re: /\bgh[posur]_[A-Za-z0-9]{36,}\b/g },
|
|
24
|
+
{ name: 'google-api', re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
25
|
+
{ name: 'slack-token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
26
|
+
{ name: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g },
|
|
27
|
+
{ name: 'jwt', re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}\b/g },
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* A `text` magas-konfidenciájú secret-tokenjeit a `PLACEHOLDER`-re cseréli. Visszaadja a redaktált szöveget +
|
|
31
|
+
* a redakciók számát (a per-fájl audit-loghoz). Ha nincs találat → az eredeti szöveg + `count: 0` (no-op).
|
|
32
|
+
*/
|
|
33
|
+
static redact(text) {
|
|
34
|
+
let count = 0;
|
|
35
|
+
let result = text;
|
|
36
|
+
for (const pattern of FAM_SecretContent_Util.PATTERNS) {
|
|
37
|
+
result = result.replace(pattern.re, () => {
|
|
38
|
+
count++;
|
|
39
|
+
return FAM_SecretContent_Util.PLACEHOLDER;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return { text: result, count: count };
|
|
43
|
+
}
|
|
44
|
+
/** `true`, ha a `text` legalább egy magas-konfidenciájú secret-tokent tartalmaz (a `redact`-re épül, állapotmentes). */
|
|
45
|
+
static hasSecret(text) {
|
|
46
|
+
return FAM_SecretContent_Util.redact(text).count > 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.FAM_SecretContent_Util = FAM_SecretContent_Util;
|
|
@@ -45,6 +45,14 @@ class FAM_SecretExclude_Util {
|
|
|
45
45
|
/secret/i, // *secret* (csak nem-forrás kiterjesztésen)
|
|
46
46
|
/credential/i, // *credential* (csak nem-forrás kiterjesztésen)
|
|
47
47
|
];
|
|
48
|
+
/**
|
|
49
|
+
* EXPLICIT key-store fájlnevek (user-FR 2026-06-21) — kiterjesztéstől függetlenül kizárnak, mert a fájl MAGA egy
|
|
50
|
+
* kulcs-tár (élő lelet: egy OpenAI-kulcs `gpt-keys.const.ts`-ben). TIGHT lista (api/gpt/secret/access + `key(s)`)
|
|
51
|
+
* → alacsony false-positive (egy general `environment.ts`-t NEM zár ki — abban a tartalmi redaktor fogja a kulcsot).
|
|
52
|
+
*/
|
|
53
|
+
static SECRET_KEYSTORE_NAME_PATTERNS = [
|
|
54
|
+
/(^|[-_.])(api|gpt|secret|access|private|auth)[-_]?keys?([-_.]|$)/i, // gpt-keys.const.ts, api-keys.ts, apikeys.js
|
|
55
|
+
];
|
|
48
56
|
/** Ismert FORRÁS-/DOC-kiterjesztések — ezeken a SOFT névrész-minta NEM zár ki (legit kód/doc). */
|
|
49
57
|
static SOURCE_DOC_EXTENSION = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|md|markdown)$/i;
|
|
50
58
|
/** A secret-mappák (az útvonal BÁRMELY szegmense — pl. `secrets/`, `.ssh/`). */
|
|
@@ -67,6 +75,10 @@ class FAM_SecretExclude_Util {
|
|
|
67
75
|
if (FAM_SecretExclude_Util.HARD_SECRET_PATTERNS.some((pattern) => pattern.test(fileName))) {
|
|
68
76
|
return true;
|
|
69
77
|
}
|
|
78
|
+
// (2b) EXPLICIT key-store fájlnév (api-/gpt-/secret-keys…) — kiterjesztéstől függetlenül (a fájl maga kulcs-tár).
|
|
79
|
+
if (FAM_SecretExclude_Util.SECRET_KEYSTORE_NAME_PATTERNS.some((pattern) => pattern.test(fileName))) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
70
82
|
// (3) SOFT névrész-minta — CSAK ha NEM ismert forrás-/doc-kiterjesztés (a "secret"-nevű .ts legit kód).
|
|
71
83
|
if (FAM_SecretExclude_Util.SOURCE_DOC_EXTENSION.test(fileName)) {
|
|
72
84
|
return false;
|
|
@@ -14,8 +14,11 @@ const fam_entry_data_model_1 = require("../../../_models/data-models/fam-entry.d
|
|
|
14
14
|
const fam_reference_code_util_1 = require("../../../_collections/fam-reference-code.util");
|
|
15
15
|
const embedding_1 = require("../../embedding");
|
|
16
16
|
const scope_reference_1 = require("../../scope-reference");
|
|
17
|
+
const fam_cooldown_util_1 = require("../../../_collections/fam-cooldown.util");
|
|
17
18
|
const fam_content_hash_util_1 = require("../_collections/fam-content-hash.util");
|
|
18
19
|
const fam_git_repo_util_1 = require("../_collections/fam-git-repo.util");
|
|
20
|
+
const fam_scan_reconcile_util_1 = require("../_collections/fam-scan-reconcile.util");
|
|
21
|
+
const fam_secret_content_util_1 = require("../_collections/fam-secret-content.util");
|
|
19
22
|
const fam_project_identity_util_1 = require("../_collections/fam-project-identity.util");
|
|
20
23
|
const fam_scan_path_util_1 = require("../_collections/fam-scan-path.util");
|
|
21
24
|
const fam_scan_progress_util_1 = require("../_collections/fam-scan-progress.util");
|
|
@@ -186,6 +189,11 @@ class FAM_Ingest_ControlService {
|
|
|
186
189
|
// Futás-közbeni progress (descriptive user feedback): a NAGY (workspace-szintű) scan haladását
|
|
187
190
|
// throttle-olt `[famIngest]` sorokkal jelezzük a konzolon (a `dryRun` NEM embeddel → ott nincs progress).
|
|
188
191
|
const progressIntervalMs = await this.resolveScanNumber('scan.progressIntervalMs', canonicalScope, 2000);
|
|
192
|
+
// Kikapcsolható szekció-cooldown (user-FR): a NAGY scan ne terhelje túl a gépet — a batch-ek KÖZÉ
|
|
193
|
+
// opcionális szünet (default 0 = ki; dryRun-on sosem várunk, mert ott nincs valódi embed-munka).
|
|
194
|
+
const sectionCooldownMs = isDryRun
|
|
195
|
+
? 0
|
|
196
|
+
: fam_cooldown_util_1.FAM_Cooldown_Util.resolveMs((await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.sectionCooldownMs', { scopePath: canonicalScope })).value);
|
|
189
197
|
let lastProgressAt = startedAt;
|
|
190
198
|
for (let i = 0; i < total; i += flowLimit) {
|
|
191
199
|
const batch = discovery.routedFiles.slice(i, i + flowLimit);
|
|
@@ -212,13 +220,19 @@ class FAM_Ingest_ControlService {
|
|
|
212
220
|
verdicts: verdicts, elapsedSeconds: (now - startedAt) / 1000,
|
|
213
221
|
}));
|
|
214
222
|
}
|
|
223
|
+
// Szekció-cooldown: a batch-ek KÖZÉ (NEM az utolsó után) — opcionális gép-tehermentesítő szünet.
|
|
224
|
+
if (sectionCooldownMs > 0 && processed < total) {
|
|
225
|
+
await fam_cooldown_util_1.FAM_Cooldown_Util.sleep(sectionCooldownMs);
|
|
226
|
+
}
|
|
215
227
|
}
|
|
216
228
|
// (4b) Törölt-fájl reconciliation (whole-file orphan) — CSAK folder/project scan (a `scan-file`
|
|
217
229
|
// egyetlen fájlt céloz, nem reconciliál). A lemezről eltűnt fájlok chunkjai soft-delete-elődnek; a
|
|
218
230
|
// számuk a `verdicts.deleted`-be (a per-fájl, fájlon-belüli `deleted`-ek mellé).
|
|
219
231
|
if (request.operation !== 'scan-file') {
|
|
232
|
+
const reconcileIgnored = await this.resolveReconcileIgnored(canonicalScope);
|
|
220
233
|
verdicts.deleted += await this.reconcileDeletedFiles({
|
|
221
234
|
discovery: discovery, scopePath: canonicalScope, issuer: issuer, dryRun: request.dryRun ?? false,
|
|
235
|
+
reconcileIgnored: reconcileIgnored,
|
|
222
236
|
});
|
|
223
237
|
}
|
|
224
238
|
// (4c) File-rendszer összefoglaló (knowledge) — generált „mi van a projektben" entry a mappastruktúrából
|
|
@@ -272,7 +286,15 @@ class FAM_Ingest_ControlService {
|
|
|
272
286
|
async processFile(set) {
|
|
273
287
|
const table = set.file.route;
|
|
274
288
|
try {
|
|
275
|
-
const
|
|
289
|
+
const rawContent = fs.readFileSync(set.file.absolutePath, 'utf-8');
|
|
290
|
+
// Tartalmi secret-redakció (user-FR): a forrásba hardcode-olt API-kulcs (sk-/AKIA/ghp_/PEM/JWT) a tárolt
|
|
291
|
+
// szöveg/embedding ELŐTT redaktálódik → a titok sose kerül a DB-be (a contentHash is a redaktáltból → egy
|
|
292
|
+
// korábban-szivárgott fájl RE-SCANKOR `modified` → auto-gyógyul). A redaktálatlan kód-kontextus megmarad.
|
|
293
|
+
const redaction = fam_secret_content_util_1.FAM_SecretContent_Util.redact(rawContent);
|
|
294
|
+
if (redaction.count > 0) {
|
|
295
|
+
fsm_dynamo_1.DyFM_Log.warn(`[famIngest] secret-redakció: ${redaction.count} token redaktálva (${set.file.relativePath})`);
|
|
296
|
+
}
|
|
297
|
+
const content = redaction.text;
|
|
276
298
|
// FAM-REV-064 bináris-tartalom guard: a denylist-en átcsúszott, ismeretlen-kiterjesztésű bináris
|
|
277
299
|
// (null-byte az első ~8KB-ban) NE kerüljön a generic chunkerbe (szemét-chunk ellen). Skip → 0 chunk.
|
|
278
300
|
if (FAM_Ingest_ControlService.isLikelyBinary(content)) {
|
|
@@ -594,15 +616,15 @@ class FAM_Ingest_ControlService {
|
|
|
594
616
|
* így egy al-mappa scan a tágabb gyökérről ingestelt, de a saját subtree-jébe eső fájlokat is helyesen
|
|
595
617
|
* reconciliálja (és kívülre sosem nyúl). Az `absolutePath` nélküli (régi, migrálatlan) entry-ket kihagyja
|
|
596
618
|
* (no-migration safe). `dryRun` → csak számol. Visszaadja a reconciliált (soft-deleted) chunkok számát.
|
|
619
|
+
*
|
|
620
|
+
* **`reconcileIgnored` (user-FR 2026-06-21, default true):** a most ignore-mintával (hard/config/`.fdpfamignore`)
|
|
621
|
+
* kizárttá vált, de KORÁBBAN beolvasott fájlok is orphanná válnak → a régi chunkjaik kitakarítódnak (ne ragadjon
|
|
622
|
+
* be, amit ignore-olni akarunk). A present-set számítása a `FAM_ScanReconcile_Util.computePresentPaths`-ban (pure).
|
|
597
623
|
*/
|
|
598
624
|
async reconcileDeletedFiles(set) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
for (const skipped of set.discovery.skippedFiles) {
|
|
604
|
-
presentPaths.add(skipped.absolutePath);
|
|
605
|
-
}
|
|
625
|
+
// A present-set (lemezen megtalált fájlok). `reconcileIgnored` → a MOST ignore-mintával kizárt fájl NEM present
|
|
626
|
+
// (a régi chunkja orphan → kitakarítható); a többi skip (secret/méret/unrouted) present marad (FAM_ScanReconcile_Util).
|
|
627
|
+
const presentPaths = fam_scan_reconcile_util_1.FAM_ScanReconcile_Util.computePresentPaths(set.discovery.routedFiles, set.discovery.skippedFiles, set.reconcileIgnored);
|
|
606
628
|
const leafScopeId = set.scopePath.length ? set.scopePath[set.scopePath.length - 1].scopeId : undefined;
|
|
607
629
|
let reconciled = 0;
|
|
608
630
|
for (const table of set.discovery.tables) {
|
|
@@ -651,6 +673,11 @@ class FAM_Ingest_ControlService {
|
|
|
651
673
|
const value = typeof resolved.value === 'number' ? resolved.value : 20;
|
|
652
674
|
return Math.min(Math.max(value, 1), 200);
|
|
653
675
|
}
|
|
676
|
+
/** Az ignore-add reconciliation (most-kizárt fájlok kitakarítása) a config-ból (`scan.reconcileIgnored`, default true). */
|
|
677
|
+
async resolveReconcileIgnored(scopePath) {
|
|
678
|
+
const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.reconcileIgnored', { scopePath: scopePath });
|
|
679
|
+
return typeof resolved.value === 'boolean' ? resolved.value : true;
|
|
680
|
+
}
|
|
654
681
|
/** Egy scan-szám-config feloldása a scope-kontextussal + fallback-kel (a `scan.estimateMsPerChunk` / `scan.progressIntervalMs`-hez). */
|
|
655
682
|
async resolveScanNumber(key, scopePath, fallback) {
|
|
656
683
|
const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve(key, { scopePath: scopePath });
|
|
@@ -51,16 +51,35 @@ class FAM_Scan_ControlService {
|
|
|
51
51
|
if (famIgnore.length) {
|
|
52
52
|
fsm_dynamo_1.DyFM_Log.info(`[famIngest] .fdpfamignore betöltve (${resolvedRoot}): ${famIgnore.length / 2} minta-sor`);
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
// KÉT külön ignore-réteg: a `hardIgnore` (config-default `node_modules`/`.git`/`build`/`dist`/… + config +
|
|
55
|
+
// input-exclude) — ezt a `!`-re-include SOHA nem éleszti fel (különben a `!client/` a `client/node_modules`-t
|
|
56
|
+
// is visszahozná → katasztrofális walk). A `softIgnore` (a .fdpfamignore `*`/saját mintái) — EZT írhatja felül
|
|
57
|
+
// a negáció. Így a „minden-kivéve-X" a hard-defaultokat tiszteletben tartja (eleve-tisztán-szkennelés).
|
|
58
|
+
const hardIgnore = baseIgnore;
|
|
59
|
+
const softIgnore = famIgnore;
|
|
60
|
+
// Re-include (`!`) szabályok a .fdpfamignore-ból (negáció — user-FR): a fájl-szintű re-include-match (`globs`)
|
|
61
|
+
// + a könyvtár-prune lineage-bázisok (`bases`). Üres a scan-file-nál / ha nincs negáció.
|
|
62
|
+
const reinclude = set.operation === 'scan-file'
|
|
63
|
+
? { globs: [], bases: [] }
|
|
64
|
+
: fam_famignore_util_1.FAM_FamIgnore_Util.loadReincludeRules(resolvedRoot);
|
|
55
65
|
const maxFileSizeBytes = await this.resolveMaxFileSize(set.scopePath);
|
|
56
66
|
const followSymlinks = await this.resolveFollowSymlinks(set.scopePath);
|
|
57
67
|
const testFileWeight = await this.resolveTestFileWeight(set.scopePath);
|
|
68
|
+
// NESTED `.fdpfamignore` (config-kapcsolós, default ON): a bejárás közben minden al-mappa saját famignore-ja
|
|
69
|
+
// is érvényesül (a saját al-útjához horgonyozva). A `collected` a walk-közben FELFEDEZETT nested-mintákat
|
|
70
|
+
// gyűjti, hogy a MÁSODIK (classify) kör is lássa őket (a minták al-út-horgonyzottak → globálisan egyértelműek).
|
|
71
|
+
const nestedEnabled = set.operation === 'scan-file' ? false : await this.resolveNestedFamignore(set.scopePath);
|
|
72
|
+
const collected = { soft: [], reGlobs: [], reBases: [] };
|
|
58
73
|
const routedFiles = [];
|
|
59
74
|
const skippedFiles = [];
|
|
60
75
|
// A felderített abszolút fájlok (file = 1 db; folder/project = rekurzív bejárás).
|
|
61
76
|
const absoluteFiles = set.operation === 'scan-file'
|
|
62
77
|
? [resolvedRoot]
|
|
63
|
-
: this.walkDirectory(resolvedRoot, resolvedRoot,
|
|
78
|
+
: this.walkDirectory(resolvedRoot, resolvedRoot, hardIgnore, softIgnore, reinclude, followSymlinks, { enabled: nestedEnabled, collected: collected });
|
|
79
|
+
// A classify-kör effektív soft-ignore + re-include halmaza: a gyökér-szintű + MINDEN walk-közben felfedezett
|
|
80
|
+
// nested minta (a nested fájl-szintű skip-ekhez — a dir-prune-t a walk már elvégezte). Üres collector → no-op.
|
|
81
|
+
const classifySoft = collected.soft.length ? [...softIgnore, ...collected.soft] : softIgnore;
|
|
82
|
+
const classifyReGlobs = collected.reGlobs.length ? [...reinclude.globs, ...collected.reGlobs] : reinclude.globs;
|
|
64
83
|
for (const absolutePath of absoluteFiles) {
|
|
65
84
|
const relativePath = set.operation === 'scan-file'
|
|
66
85
|
? path.basename(resolvedRoot)
|
|
@@ -68,7 +87,9 @@ class FAM_Scan_ControlService {
|
|
|
68
87
|
const routed = this.classifyFile({
|
|
69
88
|
absolutePath: absolutePath,
|
|
70
89
|
relativePath: relativePath,
|
|
71
|
-
|
|
90
|
+
hardIgnorePatterns: hardIgnore,
|
|
91
|
+
softIgnorePatterns: classifySoft,
|
|
92
|
+
reincludePatterns: classifyReGlobs,
|
|
72
93
|
includePatterns: set.include,
|
|
73
94
|
maxFileSizeBytes: maxFileSizeBytes,
|
|
74
95
|
tableOverride: set.tableOverride,
|
|
@@ -103,9 +124,16 @@ class FAM_Scan_ControlService {
|
|
|
103
124
|
if (fam_secret_exclude_util_1.FAM_SecretExclude_Util.isSecret(set.relativePath)) {
|
|
104
125
|
return { ...base, route: 'skip', skipReason: 'secret-exclude (filename/path-based; content-PII scan = BACKLOG)' };
|
|
105
126
|
}
|
|
106
|
-
// (
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
// (2a) HARD ignore (dsgn-004 §6.2) — node_modules/.git/build/dist/lock/… + a config + input-exclude.
|
|
128
|
+
// Ezt a `!`-re-include SOHA nem éleszti fel (a `!client/` ne hozza vissza a `client/node_modules`-t).
|
|
129
|
+
if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.hardIgnorePatterns)) {
|
|
130
|
+
return { ...base, route: 'skip', skipReason: 'ignore-pattern', excludedByIgnore: true };
|
|
131
|
+
}
|
|
132
|
+
// (2b) SOFT ignore (.fdpfamignore saját mintái) — EZT a `!`-RE-INCLUDE felülírja: soft-ignore-match SKIP,
|
|
133
|
+
// KIVÉVE ha re-include-match (negáció). Így a „minden-kivéve-X" csak a famignore-szintet vonja vissza.
|
|
134
|
+
if (set.softIgnorePatterns && fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.softIgnorePatterns)
|
|
135
|
+
&& !(set.reincludePatterns && fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.reincludePatterns))) {
|
|
136
|
+
return { ...base, route: 'skip', skipReason: 'ignore-pattern (famignore)', excludedByIgnore: true };
|
|
109
137
|
}
|
|
110
138
|
// (3) Include-szűrés (ha megadva: csak az illeszkedő fájl megy tovább, dsgn-003 §3.1).
|
|
111
139
|
if (set.includePatterns && set.includePatterns.length && !fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.includePatterns)) {
|
|
@@ -133,8 +161,13 @@ class FAM_Scan_ControlService {
|
|
|
133
161
|
* KÖNYVTÁRAKAT bejárás KÖZBEN kihagyja (ne lépjen `node_modules`-ba — teljesítmény + biztonság).
|
|
134
162
|
* A symlinkeket alapból NEM követi (`followSymlinks` config); a path-traversal guardot minden
|
|
135
163
|
* fájlra futtatja (a feloldott valódi útvonal a gyökér alatt kell maradjon).
|
|
164
|
+
*
|
|
165
|
+
* **NESTED `.fdpfamignore` (`nested.enabled`):** minden NEM-gyökér mappánál betölti a mappa saját famignore-ját
|
|
166
|
+
* (a mappa al-útjához horgonyozva), MERGE-eli az örökölt soft-ignore/re-include-ba (a leszármazottak öröklik), és
|
|
167
|
+
* a `nested.collected`-be is gyűjti (a discover második, classify-köre fájl-szinten is alkalmazza). A gyökér
|
|
168
|
+
* famignore-ját a hívó (`discover`) tölti — itt csak az al-mappákét.
|
|
136
169
|
*/
|
|
137
|
-
walkDirectory(currentDir, resolvedRoot,
|
|
170
|
+
walkDirectory(currentDir, resolvedRoot, hardIgnore, softIgnore, reinclude, followSymlinks, nested) {
|
|
138
171
|
const result = [];
|
|
139
172
|
let entries;
|
|
140
173
|
try {
|
|
@@ -144,6 +177,24 @@ class FAM_Scan_ControlService {
|
|
|
144
177
|
// Olvashatatlan könyvtár → kihagyjuk (a per-fájl olvasás-hiba a chunk-fázisban kezelt).
|
|
145
178
|
return result;
|
|
146
179
|
}
|
|
180
|
+
// NESTED famignore: a NEM-gyökér mappa saját `.fdpfamignore`-ja az al-útjához horgonyozva → MERGE az örököltbe
|
|
181
|
+
// (a leszármazottak ezt öröklik) + gyűjtés a classify-körhöz. A gyökér famignore-ját már a `discover` betöltötte.
|
|
182
|
+
let effSoftIgnore = softIgnore;
|
|
183
|
+
let effReinclude = reinclude;
|
|
184
|
+
if (nested.enabled && currentDir !== resolvedRoot) {
|
|
185
|
+
const baseDir = fam_scan_path_util_1.FAM_ScanPath_Util.toRelative(resolvedRoot, currentDir);
|
|
186
|
+
const nestedIgnore = fam_famignore_util_1.FAM_FamIgnore_Util.loadFromDir(currentDir, baseDir);
|
|
187
|
+
const nestedReinc = fam_famignore_util_1.FAM_FamIgnore_Util.loadReincludeRules(currentDir, baseDir);
|
|
188
|
+
if (nestedIgnore.length) {
|
|
189
|
+
effSoftIgnore = [...softIgnore, ...nestedIgnore];
|
|
190
|
+
nested.collected.soft.push(...nestedIgnore);
|
|
191
|
+
}
|
|
192
|
+
if (nestedReinc.globs.length) {
|
|
193
|
+
effReinclude = { globs: [...reinclude.globs, ...nestedReinc.globs], bases: [...reinclude.bases, ...nestedReinc.bases] };
|
|
194
|
+
nested.collected.reGlobs.push(...nestedReinc.globs);
|
|
195
|
+
nested.collected.reBases.push(...nestedReinc.bases);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
147
198
|
for (const entry of entries) {
|
|
148
199
|
const entryPath = path.join(currentDir, entry.name);
|
|
149
200
|
const relativePath = fam_scan_path_util_1.FAM_ScanPath_Util.toRelative(resolvedRoot, entryPath);
|
|
@@ -152,16 +203,26 @@ class FAM_Scan_ControlService {
|
|
|
152
203
|
continue;
|
|
153
204
|
}
|
|
154
205
|
if (entry.isDirectory()) {
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
206
|
+
// HARD ignore (node_modules/.git/build/…): a könyvtárat bejárás közben FELTÉTEL NÉLKÜL kihagyja —
|
|
207
|
+
// a `!`-re-include NEM élesztheti fel (a `!client/` ne lépjen `client/node_modules`-ba). Ez tartja a
|
|
208
|
+
// walkot eleve-tisztán (a katasztrofális node_modules-bejárás kizárva még re-included dir alatt is).
|
|
209
|
+
if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath, hardIgnore)
|
|
210
|
+
|| fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath + '/', hardIgnore)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// SOFT ignore (.fdpfamignore saját mintái): a mappát kihagyja — DE re-include-tudatosan: egy soft-ignore-olt
|
|
214
|
+
// mappát NEM vágunk le, ha re-included elem van rajta/alatta/fölötte (különben a `*` + `!client/` esetén a
|
|
215
|
+
// `client` szülőjét/magát levágnánk → a re-included client soha nem érhető el).
|
|
216
|
+
const softIgnored = fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath, effSoftIgnore)
|
|
217
|
+
|| fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath + '/', effSoftIgnore);
|
|
218
|
+
if (softIgnored && !fam_famignore_util_1.FAM_FamIgnore_Util.dirOnReincludeLineage(relativePath, effReinclude.globs, effReinclude.bases)) {
|
|
158
219
|
continue;
|
|
159
220
|
}
|
|
160
221
|
// Path-traversal guard (a feloldott valódi útvonal a gyökér alatt kell maradjon).
|
|
161
222
|
if (!this.isInsideRootSafe(resolvedRoot, entryPath)) {
|
|
162
223
|
continue;
|
|
163
224
|
}
|
|
164
|
-
result.push(...this.walkDirectory(entryPath, resolvedRoot,
|
|
225
|
+
result.push(...this.walkDirectory(entryPath, resolvedRoot, hardIgnore, effSoftIgnore, effReinclude, followSymlinks, nested));
|
|
165
226
|
continue;
|
|
166
227
|
}
|
|
167
228
|
if (entry.isFile()) {
|
|
@@ -225,6 +286,11 @@ class FAM_Scan_ControlService {
|
|
|
225
286
|
const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.followSymlinks', { scopePath: scopePath });
|
|
226
287
|
return typeof resolved.value === 'boolean' ? resolved.value : false;
|
|
227
288
|
}
|
|
289
|
+
/** A nested (per-alkönyvtár) `.fdpfamignore` figyelembevétele a config-ból (`scan.nestedFamignore`, default true). */
|
|
290
|
+
async resolveNestedFamignore(scopePath) {
|
|
291
|
+
const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.nestedFamignore', { scopePath: scopePath });
|
|
292
|
+
return typeof resolved.value === 'boolean' ? resolved.value : true;
|
|
293
|
+
}
|
|
228
294
|
/** A teszt-/spec-fájlok default súlya a config-ból (`scan.testFileWeight`, default 0.5 — FAM-REV-058). */
|
|
229
295
|
async resolveTestFileWeight(scopePath) {
|
|
230
296
|
const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.testFileWeight', { scopePath: scopePath });
|
|
@@ -4,6 +4,7 @@ exports.Api_Controller = void 0;
|
|
|
4
4
|
const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
|
|
5
5
|
const nts_dynamo_1 = require("@futdevpro/nts-dynamo");
|
|
6
6
|
const fam_version_const_1 = require("../../../_collections/fam-version.const");
|
|
7
|
+
const fam_heap_guard_control_service_1 = require("../../../_collections/fam-heap-guard.control-service");
|
|
7
8
|
const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
|
|
8
9
|
const fam_config_level_type_enum_1 = require("../../../_enums/fam-config-level.type-enum");
|
|
9
10
|
const config_control_service_1 = require("../config/config.control-service");
|
|
@@ -104,6 +105,9 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
|
|
|
104
105
|
async (req, res) => {
|
|
105
106
|
await this.run(res, async () => {
|
|
106
107
|
const input = (req.body ?? {});
|
|
108
|
+
// Heap-load-shed (OOM-hibakezelés): a NEHÉZ write-eket (scan/import/re-embed) critical
|
|
109
|
+
// memória-nyomásnál elutasítjuk (503), MIELŐTT egy nagy allokáció átlökné a plafont.
|
|
110
|
+
this.assertHeapCapacity(String(input.operation));
|
|
107
111
|
return mcp_1.FAM_WriteTool_Service.getInstance().handle(input);
|
|
108
112
|
});
|
|
109
113
|
},
|
|
@@ -137,7 +141,13 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
|
|
|
137
141
|
endpoint: '/health',
|
|
138
142
|
tasks: [
|
|
139
143
|
async (req, res) => {
|
|
140
|
-
|
|
144
|
+
// A heap-nyomás IS kimegy a health-en (observability — a CLI/UI/agent látja a memória-állapotot).
|
|
145
|
+
const memory = fam_heap_guard_control_service_1.FAM_HeapGuard_ControlService.getInstance().pressure();
|
|
146
|
+
res.send({
|
|
147
|
+
ok: true, service: 'fdp-agent-memory', version: fam_version_const_1.FAM_VERSION, uptime: process.uptime(),
|
|
148
|
+
memory: { level: memory.level, usedMb: memory.usedMb, limitMb: memory.limitMb,
|
|
149
|
+
ratio: Math.round(memory.ratio * 100) / 100, gcFraction: Math.round(memory.gcFraction * 100) / 100 },
|
|
150
|
+
});
|
|
141
151
|
},
|
|
142
152
|
],
|
|
143
153
|
});
|
|
@@ -406,11 +416,37 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
|
|
|
406
416
|
res.status(status).send({ ok: false, error: { errorCode: errorCode, message: message } });
|
|
407
417
|
}
|
|
408
418
|
}
|
|
409
|
-
/** A FAM hibakód → HTTP-státusz (a user/validation-hibák 400, a többi 500; dsgn-008). */
|
|
419
|
+
/** A FAM hibakód → HTTP-státusz (a memória-shed 503, a user/validation-hibák 400, a többi 500; dsgn-008). */
|
|
410
420
|
httpStatusForCode(errorCode) {
|
|
421
|
+
if (errorCode.startsWith('FAM-MEM-SHED-')) {
|
|
422
|
+
return 503;
|
|
423
|
+
}
|
|
411
424
|
const userPrefixes = ['FAM-VAL-', 'FAM-REF-RESOLVE-', 'FAM-SCOPE-WRITE-', 'FAM-MCP-DISPATCH-'];
|
|
412
425
|
return userPrefixes.some((prefix) => errorCode.startsWith(prefix)) ? 400 : 500;
|
|
413
426
|
}
|
|
427
|
+
/** A NEHÉZ write-ek (scan/import/re-embed) listája — a load-shed CSAK ezeket utasítja el (a könnyűek futnak). */
|
|
428
|
+
static HEAVY_WRITE_OPERATIONS = ['scan-file', 'scan-folder', 'scan-project', 'import', 're-embed'];
|
|
429
|
+
/**
|
|
430
|
+
* Heap-load-shed (OOM-hibakezelés): ha a `operation` NEHÉZ ÉS a heap-guard critical nyomást jelez → 503 `DyFM_Error`
|
|
431
|
+
* (a könnyű create/update/delete fut tovább). Így egy újabb nagy scan/import nem löki OOM-ba a már-feszített heapet.
|
|
432
|
+
*/
|
|
433
|
+
assertHeapCapacity(operation) {
|
|
434
|
+
if (!Api_Controller.HEAVY_WRITE_OPERATIONS.includes(operation)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const guard = fam_heap_guard_control_service_1.FAM_HeapGuard_ControlService.getInstance();
|
|
438
|
+
if (!guard.shouldShedLoad()) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const pressure = guard.pressure();
|
|
442
|
+
throw new fsm_dynamo_1.DyFM_Error({
|
|
443
|
+
errorCode: 'FAM-MEM-SHED-001',
|
|
444
|
+
message: `Memória-nyomás (${pressure.usedMb}/${pressure.limitMb} MB, ${Math.round(pressure.ratio * 100)}%, `
|
|
445
|
+
+ `GC ${Math.round(pressure.gcFraction * 100)}%) — a nehéz '${operation}' művelet ideiglenesen elutasítva `
|
|
446
|
+
+ '(load-shed). Próbáld újra, amikor a nyomás enyhül (lásd /api/logs), vagy szakaszold a scant (cooldown).',
|
|
447
|
+
issuer: this.issuer,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
414
450
|
/** A `:table` path-param → `FAM_Table` (ismeretlen → hiba). */
|
|
415
451
|
parseTable(raw) {
|
|
416
452
|
const match = Object.values(fam_table_type_enum_1.FAM_Table).find((table) => table === raw);
|
package/build/src/app.server.js
CHANGED
|
@@ -12,7 +12,10 @@ const errors_controller_1 = require("./_routes/server/errors/errors.controller")
|
|
|
12
12
|
const errors_control_service_1 = require("./_routes/server/errors/errors.control-service");
|
|
13
13
|
const server_status_controller_1 = require("./_routes/server/server-status/server-status.controller");
|
|
14
14
|
const embedding_1 = require("./_modules/embedding");
|
|
15
|
+
const config_control_service_1 = require("./_routes/server/config/config.control-service");
|
|
16
|
+
const fam_heap_guard_control_service_1 = require("./_collections/fam-heap-guard.control-service");
|
|
15
17
|
const environment_1 = require("./environments/environment");
|
|
18
|
+
const title_const_1 = require("./_collections/title.const");
|
|
16
19
|
/**
|
|
17
20
|
* FDP Agent Memory Server App — Pure Dynamo, **AUTH NÉLKÜL** (ADR-004).
|
|
18
21
|
*
|
|
@@ -32,6 +35,7 @@ class App extends nts_dynamo_1.DyNTS_App {
|
|
|
32
35
|
const mongoBase = process.env.MONGODB_URI || process.env.MONGO_URL || 'mongodb://0.0.0.0:29017';
|
|
33
36
|
return new nts_dynamo_1.DyNTS_App_Params({
|
|
34
37
|
name: 'FDP Agent Memory Server',
|
|
38
|
+
title: title_const_1.FAM_title,
|
|
35
39
|
version: package_json_1.version,
|
|
36
40
|
dbName: dbName,
|
|
37
41
|
dbUri: `${mongoBase}/${dbName}`,
|
|
@@ -40,7 +44,7 @@ class App extends nts_dynamo_1.DyNTS_App {
|
|
|
40
44
|
}
|
|
41
45
|
overrideDynamoNTSGlobalSettings() {
|
|
42
46
|
nts_dynamo_1.DyNTS_global_settings.log_settings.api_errors = false;
|
|
43
|
-
|
|
47
|
+
//DyNTS_global_settings.log_settings.setup = true;
|
|
44
48
|
// NINCS JWT/auth check (ADR-004): a szerver lokális, single-user. Az auth-t EXPLICITEN
|
|
45
49
|
// opcionálisnak jelöljük → elnémítja a minden booton megjelenő „Unique Authentication Service
|
|
46
50
|
// missing!" figyelmeztetést (a no-auth a SZÁNDÉK, nem hiányosság).
|
|
@@ -115,7 +119,33 @@ class App extends nts_dynamo_1.DyNTS_App {
|
|
|
115
119
|
* át, amit a write/scan path (MP-4/MP-5) hív mentés után. Lásd a `_modules/embedding` modul-doksit.
|
|
116
120
|
*/
|
|
117
121
|
async postProcess() {
|
|
122
|
+
// Heap-guard (OOM-hibakezelés) — a DB/config UTÁN, a (memória-igényes) pool-hidratálás ELŐTT telepítve, hogy
|
|
123
|
+
// a boot-növekedést is figyelje. A küszöbök a config-ból (memory.*), majd crash-handler + watchdog. Best-effort.
|
|
124
|
+
await this.installHeapGuard();
|
|
118
125
|
await embedding_1.FAM_EmbeddingBootstrap_ControlService.getInstance().boot();
|
|
126
|
+
fsm_dynamo_1.DyFM_Log.S_info(`UI is available at: localhost:${environment_1.env.httpPort}/api/app`);
|
|
127
|
+
}
|
|
128
|
+
/** A heap-guard küszöbeinek beolvasása a config-ból + telepítés. A config-hiba NEM buktatja a bootot (default-ok). */
|
|
129
|
+
async installHeapGuard() {
|
|
130
|
+
try {
|
|
131
|
+
const config = config_control_service_1.FAM_Config_ControlService.getInstance();
|
|
132
|
+
const num = async (key) => {
|
|
133
|
+
const resolved = await config.resolve(key, {});
|
|
134
|
+
return typeof resolved.value === 'number' ? resolved.value : undefined;
|
|
135
|
+
};
|
|
136
|
+
const enabledResolved = await config.resolve('memory.guardEnabled', {});
|
|
137
|
+
fam_heap_guard_control_service_1.FAM_HeapGuard_ControlService.getInstance().configure({
|
|
138
|
+
enabled: typeof enabledResolved.value === 'boolean' ? enabledResolved.value : undefined,
|
|
139
|
+
warnRatio: await num('memory.warnRatio'),
|
|
140
|
+
shedRatio: await num('memory.shedRatio'),
|
|
141
|
+
exitRatio: await num('memory.exitRatio'),
|
|
142
|
+
pollMs: await num('memory.pollMs'),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
fsm_dynamo_1.DyFM_Log.warn(`[heap-guard] config-olvasás sikertelen, default küszöbök: ${error?.message}`);
|
|
147
|
+
}
|
|
148
|
+
fam_heap_guard_control_service_1.FAM_HeapGuard_ControlService.getInstance().install();
|
|
119
149
|
}
|
|
120
150
|
getGlobalErrorHandler() {
|
|
121
151
|
return async (err, req, res, issuer) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futdevpro/fdp-agent-memory",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.21",
|
|
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": {
|