Skip to content

Commit 63e23cb

Browse files
authored
Merge pull request #368 from HelpCode-ai/fix/kg-observational-oom-2
fix(kg): bound observational ingest memory + CPU (per-page flush, walk cap)
2 parents be5ffbc + 473884f commit 63e23cb

2 files changed

Lines changed: 36 additions & 18 deletions

File tree

packages/backend/src/knowledge-graph/identifier.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,23 @@ export interface IdentifierOccurrence {
4646
value: string;
4747
}
4848

49+
/** Max JSON nodes to walk per payload (bounds CPU on huge bulk responses). */
50+
const MAX_WALK_NODES = 5000;
51+
4952
/**
5053
* Walk an arbitrary JSON value and yield (leaf-field, identifier-value) pairs.
5154
* Uses the leaf key as the field name (FK fields are leaf keys like customer_id).
5255
*/
5356
export function extractIdentifiers(input: unknown): IdentifierOccurrence[] {
5457
const out: IdentifierOccurrence[] = [];
5558
const seen = new Set<string>();
59+
// Bound work on pathologically large payloads (e.g. a bulk DB dump): a few
60+
// thousand nodes is plenty to capture representative identifiers, and stops a
61+
// single huge response from pinning the event loop.
62+
let visited = 0;
5663

5764
const walk = (node: unknown, key: string | null): void => {
65+
if (visited++ > MAX_WALK_NODES) return;
5866
if (node === null || node === undefined) return;
5967
if (Array.isArray(node)) {
6068
for (const item of node) walk(item, key);
@@ -89,7 +97,9 @@ export function extractIdentifiers(input: unknown): IdentifierOccurrence[] {
8997
*/
9098
export function extractFieldNames(input: unknown): string[] {
9199
const out = new Set<string>();
100+
let visited = 0;
92101
const walk = (node: unknown): void => {
102+
if (visited++ > MAX_WALK_NODES) return;
93103
if (node === null || node === undefined) return;
94104
if (Array.isArray(node)) {
95105
for (const item of node) walk(item);

packages/backend/src/knowledge-graph/kg-observational.service.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,9 @@ export class KgObservationalService {
8181
s.add(r.entity);
8282
}
8383

84-
const newHashes = new Set<string>();
85-
const valueRows: Array<{
86-
organizationId: string;
87-
connectorId: string;
88-
valueHash: string;
89-
entity: string;
90-
field: string;
91-
direction: string;
92-
}> = [];
84+
// Edges accumulate across pages; the raw value rows are flushed per page
85+
// (below) so the in-memory set never grows to the size of the whole batch.
86+
let edges = 0;
9387
// references edges mined from response field names: key -> details.
9488
const refBumps = new Map<
9589
string,
@@ -103,7 +97,7 @@ export class KgObservationalService {
10397
// Page through invocations so we never hold more than CHUNK full input/output
10498
// payloads in memory at once. A busy org's payloads can be megabytes each;
10599
// loading thousands together previously OOM'd the backend.
106-
const CHUNK = 200;
100+
const CHUNK = 100;
107101
let processed = 0;
108102
while (processed < MAX_INVOCATIONS_PER_RUN) {
109103
const page = await this.prisma.toolInvocation.findMany({
@@ -124,6 +118,17 @@ export class KgObservationalService {
124118
if (page.length === 0) break;
125119
processed += page.length;
126120

121+
// Per-page value occurrences, flushed at the end of each page.
122+
const newHashes = new Set<string>();
123+
const valueRows: Array<{
124+
organizationId: string;
125+
connectorId: string;
126+
valueHash: string;
127+
entity: string;
128+
field: string;
129+
direction: string;
130+
}> = [];
131+
127132
for (const inv of page) {
128133
const connectorId = inv.connectorId!;
129134
// Skip invocations already covered by this connector's watermark.
@@ -185,16 +190,19 @@ export class KgObservationalService {
185190
}
186191
}
187192
} // for (const inv of page)
188-
if (page.length < CHUNK) break;
189-
} // while pages
190193

191-
if (valueRows.length) {
192-
await this.prisma.kgValueSeen.createMany({ data: valueRows });
193-
}
194+
// Flush this page's value occurrences and correlate immediately. correlate
195+
// reads kgValueSeen (which now includes earlier pages), so cross-page
196+
// produces_consumes / same_identity links are still found.
197+
if (valueRows.length) {
198+
await this.prisma.kgValueSeen.createMany({ data: valueRows });
199+
}
200+
if (newHashes.size) {
201+
edges += await this.correlate(organizationId, [...newHashes]);
202+
}
194203

195-
let edges = newHashes.size
196-
? await this.correlate(organizationId, [...newHashes])
197-
: 0;
204+
if (page.length < CHUNK) break;
205+
} // while pages
198206

199207
// Apply references edges mined from response shapes.
200208
const refCache = new Map<string, string>();

0 commit comments

Comments
 (0)