mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 09:52:27 +00:00
fix(settings): keep Windows update mode on configuration errors
Invalid ChangeTime settings now carry update_mode and pending_restart through the configuration error and its projection, including startup parsing. Runtime recoveries name update_analysis_settings instead of telling the caller to restart REA.
This commit is contained in:
1 parent
be20bdfb26
commit
9bcc95a801
13 files changed
+95
-34
No files matched your search
@@ -1,5 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
The next release changes analysis-setting output (#9885). `update_analysis_settings`, `inspect_analysis_settings`, and limit errors no longer include `rea_restart_required`, `restart_required`, `restartRequired`, or `effect`. Read `update_mode` (`runtime` or `restart`) and `pending_restart`. There is no compatibility alias. Release Please should copy this migration into that release's breaking-changes section.
|
||||
|
||||
## [6.8.0](https://github.com/morluto/rea/compare/rea-agents-6.7.0...rea-agents-6.8.0) (2026-10-25)
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -257,8 +257,8 @@ are runtime-wide, not scoped to a target or an individual connection.
|
||||
### Failure and retry
|
||||
|
||||
An ASAR admission error supplies `details.limit` with the exact setting,
|
||||
effective value, units, bounds, scope, application point, restart requirement
|
||||
and, when known, required bytes and value source. The agent can immediately call:
|
||||
effective value, units, bounds, scope, application point, `update_mode`,
|
||||
`pending_restart` and, when known, required bytes and value source. The agent can immediately call:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -285,6 +285,14 @@ Error copying, attribution, redaction, and delivery must preserve these facts.
|
||||
Human-readable remediation explains the same failure; clients should not need
|
||||
to parse that sentence to identify an editable setting.
|
||||
|
||||
This output is breaking. Results and errors no longer carry the previous
|
||||
restart boolean or a single effect field, and there is no compatibility alias.
|
||||
Read `update_mode` to see whether the setter can change the active value, and
|
||||
`pending_restart` to see whether a configured value is waiting. A configuration
|
||||
error for a Windows ChangeTime setting carries those two fields on
|
||||
`details.settings`, not only in the constraint sentence. The removed field names
|
||||
are listed in the changelog.
|
||||
|
||||
`scope` and `applies_at` describe a continuing caller, such as an MCP
|
||||
connection. A one-shot invocation, such as a CLI command, has released its
|
||||
provider session and exited its runtime before its caller reads the failure,
|
||||
|
||||
@@ -103,11 +103,10 @@ it("uses a valid REA_CAPTURE_DECODER_OLD_GENERATION_MIB override", async () => {
|
||||
/floor 8 MiB.*maximum 16384 MiB/iu,
|
||||
);
|
||||
expect(projected.remediation.action).toContain("update_analysis_settings");
|
||||
expect(projected.remediation.action).toMatch(/restart REA/iu);
|
||||
expect(projected.remediation.action).toContain("launch seed");
|
||||
expect(projected.remediation.action).toContain(
|
||||
"capture.decoder_old_generation_mib",
|
||||
);
|
||||
expect(projected.remediation.action).toMatch(/without a REA restart/iu);
|
||||
expect(projected.remediation.action).toMatch(
|
||||
/removes inherited NODE_OPTIONS/iu,
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ export const resolveCaptureDecoderHeap = (
|
||||
export const captureDecoderHeapRecovery = (heap: CaptureDecoderHeap): string =>
|
||||
[
|
||||
`The effective ${CAPTURE_DECODER_OLD_GENERATION_SETTING} value is ${String(heap.oldGenerationMiB)} MiB (source ${heap.source}; default ${String(DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB, floor ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} MiB, maximum ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB).`,
|
||||
`Set ${CAPTURE_DECODER_OLD_GENERATION_SETTING} in the environment that starts REA to a decimal integer from ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} to ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)}, then restart REA. In a running MCP server, use update_analysis_settings with setting capture.decoder_old_generation_mib and action set instead; the new value applies to new work without a REA restart.`,
|
||||
`capture.decoder_old_generation_mib can be changed at runtime with update_analysis_settings. The launch seed is ${CAPTURE_DECODER_OLD_GENERATION_SETTING}, a decimal integer from ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} to ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)}.`,
|
||||
`On Node.js the decoder passes --max-old-space-size=${String(heap.oldGenerationMiB)} and removes inherited NODE_OPTIONS, so additional host memory and NODE_OPTIONS do not raise this heap. The ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} MiB semi-space stays fixed.`,
|
||||
"On Bun the V8 flags are ignored. Windows limits the decoder's committed memory to this value plus a 512 MiB reserve. Other Bun hosts enforce no decoder heap limit.",
|
||||
].join(" ");
|
||||
@@ -6,7 +6,10 @@ import { z } from "zod";
|
||||
import { mcpInputBudgetSchema } from "./mcpInputBudget.js";
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import {
|
||||
ConfigurationError,
|
||||
type ConfigurationSettingIssue,
|
||||
} from "../domain/configurationErrors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { analysisProviderSelectorSchema } from "../domain/providerSelection.js";
|
||||
import {
|
||||
@@ -165,6 +168,31 @@ const environmentSchema = z.object({
|
||||
REA_REFERENCE_SECRET_PATTERNS_JSON: z.string().default("[]"),
|
||||
});
|
||||
|
||||
/** Copy producer-owned update facts from a Zod issue without inventing them. */
|
||||
const configurationSettingFromIssue = (issue: {
|
||||
readonly path: readonly PropertyKey[];
|
||||
readonly message: string;
|
||||
readonly code: string;
|
||||
readonly params?: Readonly<Record<string, unknown>> | undefined;
|
||||
}): ConfigurationSettingIssue => {
|
||||
const setting = String(issue.path[0] ?? "environment");
|
||||
const constraint = issue.message;
|
||||
if (issue.code !== "custom" || issue.params === undefined)
|
||||
return { setting, constraint };
|
||||
const updateMode = issue.params.update_mode;
|
||||
const pendingRestart = issue.params.pending_restart;
|
||||
return {
|
||||
setting,
|
||||
constraint,
|
||||
...(updateMode === "runtime" || updateMode === "restart"
|
||||
? { update_mode: updateMode }
|
||||
: {}),
|
||||
...(typeof pendingRestart === "boolean"
|
||||
? { pending_restart: pendingRestart }
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
/** Parse the JEB-only timeout at its isolated provider-composition boundary. */
|
||||
export const parseJebMcpTimeoutMs = (
|
||||
value: string | undefined,
|
||||
@@ -231,10 +259,10 @@ export const parseEnvironmentValues = (
|
||||
const parsed = environmentSchema.safeParse(literals);
|
||||
if (!parsed.success) {
|
||||
// Zod messages name the constraint, never the rejected value.
|
||||
const settings = parsed.error.issues.map((issue) => ({
|
||||
setting: String(issue.path[0] ?? "environment"),
|
||||
constraint: issue.message,
|
||||
}));
|
||||
// A producer may attach update_mode and pending_restart on a custom issue.
|
||||
const settings = parsed.error.issues.map((issue) =>
|
||||
configurationSettingFromIssue(issue),
|
||||
);
|
||||
return err(
|
||||
new ConfigurationError("Invalid REA environment configuration", {
|
||||
cause: parsed.error,
|
||||
|
||||
@@ -66,8 +66,7 @@ it("keeps the 5000 ms deadline when the setting is unset", () => {
|
||||
expect(projected.remediation.action).toContain("source default");
|
||||
expect(projected.remediation.action).toContain("update_analysis_settings");
|
||||
expect(projected.remediation.action).toContain("regex.matching_deadline_ms");
|
||||
expect(projected.remediation.action).toMatch(/restart REA/i);
|
||||
expect(projected.remediation.action).toMatch(/without a REA restart/i);
|
||||
expect(projected.remediation.action).toContain("launch seed");
|
||||
expect(projected.remediation.action).toMatch(
|
||||
/heap does not raise this deadline/i,
|
||||
);
|
||||
|
||||
@@ -70,6 +70,6 @@ export const regexMatchingDeadlineRecovery = (
|
||||
): string =>
|
||||
[
|
||||
`The effective ${REGEX_MATCHING_DEADLINE_SETTING} value is ${String(deadline.deadlineMs)} ms (source ${deadline.source}; default ${String(DEFAULT_REGEX_MATCHING_DEADLINE_MS)} ms, floor 1 ms, maximum ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)} ms).`,
|
||||
`Set ${REGEX_MATCHING_DEADLINE_SETTING} in the environment that starts REA to a positive decimal integer from 1 to ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)}, then restart REA. In a running MCP server, use update_analysis_settings with setting regex.matching_deadline_ms and action set instead; the new value applies to new work without a REA restart.`,
|
||||
`regex.matching_deadline_ms can be changed at runtime with update_analysis_settings. The launch seed is ${REGEX_MATCHING_DEADLINE_SETTING}, a positive decimal integer from 1 to ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)}.`,
|
||||
"The worker old-generation heap does not raise this deadline.",
|
||||
].join(" ");
|
||||
@@ -77,8 +77,7 @@ it("keeps the 128 MiB request when the setting is unset", () => {
|
||||
);
|
||||
expect(projected.remediation.action).toContain("--max-old-space-size");
|
||||
expect(projected.remediation.action).toContain("NODE_OPTIONS");
|
||||
expect(projected.remediation.action).toMatch(/restart REA/i);
|
||||
expect(projected.remediation.action).toMatch(/without a REA restart/i);
|
||||
expect(projected.remediation.action).toContain("launch seed");
|
||||
expect(projected.remediation.action).toMatch(/overrides this request/i);
|
||||
});
|
||||
|
||||
|
||||
@@ -72,6 +72,6 @@ export const regexWorkerHeapRequestRecovery = (
|
||||
): string =>
|
||||
[
|
||||
`The effective ${REGEX_WORKER_HEAP_REQUEST_SETTING} value is ${String(request.heapMiB)} MiB (source ${request.source}; default ${String(DEFAULT_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB, floor ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB, maximum ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB).`,
|
||||
`Set ${REGEX_WORKER_HEAP_REQUEST_SETTING} in the environment that starts REA to a decimal integer from ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} to ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)}, then restart REA. In a running MCP server, use update_analysis_settings with setting regex.worker_heap_request_mib and action set instead; the new value applies to new work without a REA restart.`,
|
||||
`regex.worker_heap_request_mib can be changed at runtime with update_analysis_settings. The launch seed is ${REGEX_WORKER_HEAP_REQUEST_SETTING}, a decimal integer from ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} to ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)}.`,
|
||||
"A positive Node --max-old-space-size, including NODE_OPTIONS, overrides this request and sets the effective heap. That flag is not limited by this setting's floor or maximum. Raising host memory alone does not raise the request.",
|
||||
].join(" ");
|
||||
@@ -27,7 +27,7 @@ const requiredFacts = (
|
||||
expect(message).toContain(String(fact.defaultValue));
|
||||
expect(message).toContain(String(fact.floor));
|
||||
expect(message).toContain(String(fact.maximum));
|
||||
expect(message).toContain("restart REA");
|
||||
expect(message).toContain("positive decimal integer");
|
||||
};
|
||||
|
||||
it("keeps today's settle defaults when the settings are unset", () => {
|
||||
@@ -98,17 +98,21 @@ it.each([
|
||||
const message = resolved.error.settings[0]?.constraint;
|
||||
if (message === undefined) throw new Error("Missing configuration guidance");
|
||||
requiredFacts(message, { ...bound, currentValue: value, source: "setting" });
|
||||
expect(resolved.error.settings).toEqual([
|
||||
{ setting: bound.setting, constraint: message },
|
||||
]);
|
||||
const facts = {
|
||||
setting: bound.setting,
|
||||
constraint: message,
|
||||
update_mode: "restart" as const,
|
||||
pending_restart: true as const,
|
||||
};
|
||||
expect(resolved.error.settings).toEqual([facts]);
|
||||
expect(projectAnalysisError(resolved.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
details: { settings: [{ setting: bound.setting, constraint: message }] },
|
||||
details: { settings: [facts] },
|
||||
});
|
||||
const startup = parseEnvironment({ [bound.setting]: value });
|
||||
if (startup.ok) throw new Error(`Startup accepted ${bound.setting}=${value}`);
|
||||
expect(startup.error.settings).toContainEqual({
|
||||
setting: bound.setting,
|
||||
constraint: message,
|
||||
expect(startup.error.settings).toContainEqual(facts);
|
||||
expect(projectAnalysisError(startup.error).details).toMatchObject({
|
||||
settings: [facts],
|
||||
});
|
||||
});
|
||||
@@ -85,7 +85,7 @@ const windowsChangeTimeSettingFailure = (
|
||||
pending_restart: true,
|
||||
});
|
||||
|
||||
/** Name the rejected value, its source, the allowed range, and the required restart. */
|
||||
/** Name the rejected value, its source, and the allowed range. */
|
||||
const windowsChangeTimeSettingMessage = (
|
||||
failure: WindowsChangeTimeSettingFailure,
|
||||
): string =>
|
||||
@@ -103,11 +103,14 @@ export const windowsChangeTimeSettingSchema = (bound: WindowsChangeTimeBound) =>
|
||||
.string()
|
||||
.superRefine((value, context) => {
|
||||
if (inRange(bound, value)) return;
|
||||
const failure = windowsChangeTimeSettingFailure(bound, value);
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: windowsChangeTimeSettingMessage(
|
||||
windowsChangeTimeSettingFailure(bound, value),
|
||||
),
|
||||
message: windowsChangeTimeSettingMessage(failure),
|
||||
params: {
|
||||
update_mode: failure.update_mode,
|
||||
pending_restart: failure.pending_restart,
|
||||
},
|
||||
});
|
||||
})
|
||||
.transform(Number);
|
||||
@@ -163,6 +166,8 @@ export const resolveWindowsChangeTimeSettle = (
|
||||
settings: failures.map((failure) => ({
|
||||
setting: failure.setting,
|
||||
constraint: windowsChangeTimeSettingMessage(failure),
|
||||
update_mode: failure.update_mode,
|
||||
pending_restart: failure.pending_restart,
|
||||
})),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1045,9 +1045,15 @@ const lifecycleErrorDetails = (
|
||||
}
|
||||
if (error instanceof ConfigurationError && error.settings.length > 0)
|
||||
return {
|
||||
settings: error.settings.map(({ setting, constraint }) => ({
|
||||
setting,
|
||||
constraint,
|
||||
settings: error.settings.map((issue) => ({
|
||||
setting: issue.setting,
|
||||
constraint: issue.constraint,
|
||||
...(issue.update_mode === undefined
|
||||
? {}
|
||||
: { update_mode: issue.update_mode }),
|
||||
...(issue.pending_restart === undefined
|
||||
? {}
|
||||
: { pending_restart: issue.pending_restart }),
|
||||
})),
|
||||
};
|
||||
if (error instanceof BinaryTargetError)
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
export interface ConfigurationSettingIssue {
|
||||
readonly setting: string;
|
||||
readonly constraint: string;
|
||||
/** Whether `update_analysis_settings` can change this setting, when the producer owns that fact. */
|
||||
readonly update_mode?: "runtime" | "restart";
|
||||
/** True when the rejected value is configured but not active in this process. */
|
||||
readonly pending_restart?: boolean;
|
||||
}
|
||||
|
||||
/** Rejected settings retained for diagnostics; their values are not. */
|
||||
@@ -24,9 +28,16 @@ export class ConfigurationError extends AnalysisError {
|
||||
constructor(message: string, options?: ConfigurationErrorOptions) {
|
||||
super(message, options);
|
||||
this.remediationAction = options?.remediationAction;
|
||||
this.settings = (options?.settings ?? []).map(
|
||||
({ setting, constraint }) => ({ setting, constraint }),
|
||||
);
|
||||
this.settings = (options?.settings ?? []).map((issue) => ({
|
||||
setting: issue.setting,
|
||||
constraint: issue.constraint,
|
||||
...(issue.update_mode === undefined
|
||||
? {}
|
||||
: { update_mode: issue.update_mode }),
|
||||
...(issue.pending_restart === undefined
|
||||
? {}
|
||||
: { pending_restart: issue.pending_restart }),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user