From 9bcc95a8015e1d93e32e2d6aa152aadc5aad87dc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 10 Nov 2026 22:57:11 +0000 Subject: [PATCH] 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. --- CHANGELOG.md | 2 ++ docs/design/limits-and-recovery.md | 12 +++++-- src/config/captureDecoderHeap.test.ts | 3 +- src/config/captureDecoderHeap.ts | 2 +- src/config/environment.ts | 38 +++++++++++++++++++--- src/config/regexMatchingDeadline.test.ts | 3 +- src/config/regexMatchingDeadline.ts | 2 +- src/config/regexWorkerHeapRequest.test.ts | 3 +- src/config/regexWorkerHeapRequest.ts | 2 +- src/config/windowsChangeTimeSettle.test.ts | 20 +++++++----- src/config/windowsChangeTimeSettle.ts | 13 +++++--- src/domain/analysisErrorProjection.ts | 12 +++++-- src/domain/configurationErrors.ts | 17 ++++++++-- 13 files changed, 95 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e652f26036..5dd89a639e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/design/limits-and-recovery.md b/docs/design/limits-and-recovery.md index 7bab388bef..10a1c94a5b 100644 --- a/docs/design/limits-and-recovery.md +++ b/docs/design/limits-and-recovery.md @@ -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, diff --git a/src/config/captureDecoderHeap.test.ts b/src/config/captureDecoderHeap.test.ts index 24e8549769..daa7af5a84 100644 --- a/src/config/captureDecoderHeap.test.ts +++ b/src/config/captureDecoderHeap.test.ts @@ -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, ); diff --git a/src/config/captureDecoderHeap.ts b/src/config/captureDecoderHeap.ts index ffd1e2b9dc..c95715a537 100644 --- a/src/config/captureDecoderHeap.ts +++ b/src/config/captureDecoderHeap.ts @@ -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(" "); diff --git a/src/config/environment.ts b/src/config/environment.ts index d8b945b4f2..d2405c3373 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -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> | 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, diff --git a/src/config/regexMatchingDeadline.test.ts b/src/config/regexMatchingDeadline.test.ts index 754260557a..b61e7eae4e 100644 --- a/src/config/regexMatchingDeadline.test.ts +++ b/src/config/regexMatchingDeadline.test.ts @@ -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, ); diff --git a/src/config/regexMatchingDeadline.ts b/src/config/regexMatchingDeadline.ts index 720ce68b16..2345a09e82 100644 --- a/src/config/regexMatchingDeadline.ts +++ b/src/config/regexMatchingDeadline.ts @@ -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(" "); diff --git a/src/config/regexWorkerHeapRequest.test.ts b/src/config/regexWorkerHeapRequest.test.ts index 38df9898cc..3bcf8e6ed6 100644 --- a/src/config/regexWorkerHeapRequest.test.ts +++ b/src/config/regexWorkerHeapRequest.test.ts @@ -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); }); diff --git a/src/config/regexWorkerHeapRequest.ts b/src/config/regexWorkerHeapRequest.ts index 1d4a8dbc83..051e8226ca 100644 --- a/src/config/regexWorkerHeapRequest.ts +++ b/src/config/regexWorkerHeapRequest.ts @@ -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(" "); diff --git a/src/config/windowsChangeTimeSettle.test.ts b/src/config/windowsChangeTimeSettle.test.ts index a5656f9682..9d38d1369b 100644 --- a/src/config/windowsChangeTimeSettle.test.ts +++ b/src/config/windowsChangeTimeSettle.test.ts @@ -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], }); }); diff --git a/src/config/windowsChangeTimeSettle.ts b/src/config/windowsChangeTimeSettle.ts index 2d0112f83e..489daa7fe6 100644 --- a/src/config/windowsChangeTimeSettle.ts +++ b/src/config/windowsChangeTimeSettle.ts @@ -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, })), }, ), diff --git a/src/domain/analysisErrorProjection.ts b/src/domain/analysisErrorProjection.ts index 9b71d77493..c7244177c5 100644 --- a/src/domain/analysisErrorProjection.ts +++ b/src/domain/analysisErrorProjection.ts @@ -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) diff --git a/src/domain/configurationErrors.ts b/src/domain/configurationErrors.ts index 3295a6b949..ba455e9f44 100644 --- a/src/domain/configurationErrors.ts +++ b/src/domain/configurationErrors.ts @@ -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 }), + })); } }