mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
* feat(process): make the regex worker heap request configurable REA_REGEX_WORKER_HEAP_REQUEST_MIB keeps the 128 MiB default and refuses values outside 8 to 16384 MiB. A positive Node --max-old-space-size still overrides the request and is not clamped by the setting. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): set the regex worker heap floor to the measured startup bound On Node 22.23.4 a 5 MiB old-generation request aborts the regex worker during startup, and 6 MiB starts it and returns a match. The setting floor is that measured bound. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): raise the regex worker heap floor to 7 MiB 6 MiB passes configuration but returns ERR_WORKER_OUT_OF_MEMORY for a trivial match on supported Linux Node 24 and Node 26.0.0. 7 MiB returned that match on the measured 22, 24, and 26 lines. A clean-process test runs the production worker at the floor. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
161 lines
5.4 KiB
TypeScript
161 lines
5.4 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
|
|
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisOutputError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { analysisStringSchema } from "../domain/hopperValues.js";
|
|
import { HopperRemoteError } from "../domain/hopperErrors.js";
|
|
import type { JsonValue } from "../domain/jsonValue.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import { analysisSearchInput } from "../contracts/analysisSearchContract.js";
|
|
import type { HopperClient } from "./HopperClient.js";
|
|
import {
|
|
DEFAULT_REGEX_MATCHING_DEADLINE,
|
|
type RegexMatchingDeadline,
|
|
} from "../config/regexMatchingDeadline.js";
|
|
import {
|
|
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
|
|
type RegexWorkerHeapRequest,
|
|
} from "../config/regexWorkerHeapRequest.js";
|
|
import { matchRegexValues } from "../process/RegexMatcher.js";
|
|
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
|
|
|
|
const inputSchema = z.strictObject({
|
|
...analysisSearchInput,
|
|
mode: z.literal("regex"),
|
|
});
|
|
/** Match collected native facts off Hopper's API thread, with cancellable lifetime. */
|
|
export class HopperRegexSearch {
|
|
readonly #active = new Map<
|
|
AbortController,
|
|
Promise<Result<JsonValue, AnalysisError>>
|
|
>();
|
|
|
|
constructor(
|
|
private readonly client: Pick<HopperClient, "callTool">,
|
|
private readonly deadline: RegexMatchingDeadline = DEFAULT_REGEX_MATCHING_DEADLINE,
|
|
private readonly heapRequest: RegexWorkerHeapRequest = DEFAULT_REGEX_WORKER_HEAP_REQUEST,
|
|
) {}
|
|
|
|
execute(
|
|
operation: "search_strings" | "search_procedures",
|
|
parameters: Readonly<Record<string, JsonValue>>,
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<JsonValue, AnalysisError>> {
|
|
const controller = new AbortController();
|
|
const signal =
|
|
options?.signal === undefined
|
|
? controller.signal
|
|
: AbortSignal.any([controller.signal, options.signal]);
|
|
const pending = this.#execute(operation, parameters, {
|
|
...options,
|
|
signal,
|
|
});
|
|
this.#active.set(controller, pending);
|
|
void pending.then(
|
|
() => this.#active.delete(controller),
|
|
() => this.#active.delete(controller),
|
|
);
|
|
return pending;
|
|
}
|
|
|
|
/** Abort matching workers before their provider session closes. */
|
|
async close(): Promise<void> {
|
|
for (const controller of this.#active.keys()) controller.abort();
|
|
await Promise.all(this.#active.values());
|
|
}
|
|
|
|
async #execute(
|
|
operation: "search_strings" | "search_procedures",
|
|
parameters: Readonly<Record<string, JsonValue>>,
|
|
options: ExecutionOptions,
|
|
): Promise<Result<JsonValue, AnalysisError>> {
|
|
const input = inputSchema.safeParse(parameters);
|
|
if (!input.success)
|
|
return err(
|
|
new HopperRemoteError(
|
|
-32000,
|
|
`Invalid search arguments: ${input.error.message}`,
|
|
{ diagnosticType: "invalid_request", operation },
|
|
),
|
|
);
|
|
const inventoryOptions: ExecutionOptions = {
|
|
...options,
|
|
...(options.progress === undefined
|
|
? {}
|
|
: {
|
|
progress: {
|
|
report: (update) =>
|
|
options.progress?.report({
|
|
...update,
|
|
completed:
|
|
update.total === null || update.total === 0
|
|
? 0
|
|
: Math.min(0.5, update.completed / update.total / 2),
|
|
total: 1,
|
|
}) ?? Promise.resolve(),
|
|
},
|
|
}),
|
|
};
|
|
const inventory = await this.client.callTool(
|
|
operation === "search_strings" ? "list_strings" : "list_procedures",
|
|
input.data.document === undefined
|
|
? {}
|
|
: { document: input.data.document },
|
|
inventoryOptions,
|
|
);
|
|
if (!inventory.ok)
|
|
return err(
|
|
analysisClientFailureForOperation(inventory.error, "hopper", operation),
|
|
);
|
|
const parsed = z.array(analysisStringSchema).safeParse(inventory.value);
|
|
if (!parsed.success)
|
|
return err(
|
|
new AnalysisOutputError(
|
|
operation,
|
|
"Hopper search inventory is malformed",
|
|
),
|
|
);
|
|
if (options.signal?.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(operation, { cause: options.signal.reason }),
|
|
);
|
|
await options.progress
|
|
?.report({
|
|
phase: "hopper_regex",
|
|
completed: 0.5,
|
|
total: 1,
|
|
message: "Matching inventory in cancellable regex worker",
|
|
})
|
|
.catch(() => undefined);
|
|
if (options.signal?.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(operation, { cause: options.signal.reason }),
|
|
);
|
|
const matched = await matchRegexValues({
|
|
operation,
|
|
pattern: input.data.pattern,
|
|
caseSensitive: input.data.case_sensitive,
|
|
values: parsed.data.map((item) => item.value),
|
|
signal: options.signal,
|
|
deadline: this.deadline,
|
|
heapRequest: this.heapRequest,
|
|
invalidPattern: (reason) =>
|
|
new HopperRemoteError(-32000, `Invalid regex pattern: ${reason}`, {
|
|
diagnosticType: "invalid_request",
|
|
operation,
|
|
}),
|
|
});
|
|
if (!matched.ok) return matched;
|
|
if (options.signal?.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(operation, { cause: options.signal.reason }),
|
|
);
|
|
const selected = new Set(matched.value);
|
|
return ok(parsed.data.filter((_, index) => selected.has(index)));
|
|
}
|
|
}
|