Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include parameter in the documented update modes

The breaking-change migration says update_mode is only runtime or restart, but limit failures for artifact/web budgets and explicit JavaScript or WASM arguments now emit parameter (for example, ArtifactReaderLimits.ts and analysisSettingLimit). Clients implementing the changelog's stated enum can therefore reject valid error payloads or recommend the wrong recovery; document all three values in the release migration.

AGENTS.md reference: AGENTS.md:L99-L103

Useful? React with 👍 / 👎.


## [6.8.0](https://github.com/morluto/rea/compare/rea-agents-6.7.0...rea-agents-6.8.0) (2026-10-25)

### Highlights
Expand Down
22 changes: 17 additions & 5 deletions docs/design/limits-and-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -275,13 +275,24 @@ that argument; raising a runtime default cannot override it.

The resource owner constructs `details.limit` from the registry plus the budget
it actually enforced. Metadata includes `agent_editable: true`,
`scope: "rea-runtime"`, and `rea_restart_required: false`. Optional
`scope: "rea-runtime"`, `update_mode`, and `pending_restart`. `update_mode:
"runtime"` means `update_analysis_settings` can change the active value.
`pending_restart` is false when that reported value is the one this process is
using. Optional
`required_value`, `source`, and `parameter` fields are supplied only where known
or applicable. Do not infer a source because a value happens to equal a default.
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,
Expand All @@ -290,8 +301,9 @@ so its adapter projects with an explicit `"invocation"` caller lifetime.
`scope: "rea-invocation"`, `applies_at: "next_invocation"` and the launch
`environment` seed when one exists. `agent_editable: true` still holds, because
the caller chooses that environment value or per-call `parameter` for the new
invocation; `rea_restart_required: false` means no running REA process needs a
restart. A producer keeps guidance that holds only while its caller retains the
invocation; `update_mode: "runtime"` and `pending_restart: false` mean the
reported value is active in this process. `update_mode: "restart"` means
`update_analysis_settings` does not change it. A producer keeps guidance that holds only while its caller retains the
session, such as "the Ghidra session remains usable" or a same-session runtime
update, in `continuingCallerGuidance`; the shared presentation adds it only for
a continuing caller. The producer's own message and remediation must stay true
Expand Down
83 changes: 43 additions & 40 deletions docs/design/rea-limit-inventory.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@
"applies_at": "next_operation",
"agent_editable": true,
"scope": "rea-runtime",
"rea_restart_required": false
"update_mode": "runtime",
"pending_restart": false
},
"remediation": {
"action": "Use analyze_function (CLI: `rea function`) for listing facts. Call update_analysis_settings with setting ghidra.decompile_timeout_seconds and action set to raise the per-function deadline for the next operation on this Program. For CLI or future REA processes, set REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS. Set the MCP tool-call deadline to cover startup and the full call; earlier cancellation closes the session. `rea setup` estimates deadlines for new Codex/Kimi registrations and preserves existing positive values; edit existing deadlines yourself."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@
"applies_at": "next_operation",
"agent_editable": true,
"scope": "rea-runtime",
"rea_restart_required": false
"update_mode": "runtime",
"pending_restart": false
},
"remediation": {
"action": "Use analyze_function (CLI: `rea function`) for listing facts. Call update_analysis_settings with setting ghidra.decompile_timeout_seconds and action set to raise the per-function deadline for the next operation on this Program. For CLI or future REA processes, set REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS. Set the MCP tool-call deadline to cover startup and the full call; earlier cancellation closes the session. `rea setup` estimates deadlines for new Codex/Kimi registrations and preserves existing positive values; edit existing deadlines yourself."
Expand Down
77 changes: 70 additions & 7 deletions scripts/verify/rea-numeric-limits.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { join, relative, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

// Every numeric REA_ limit under src must be an analysis setting or a
// documented exception with a reason. restart-only is durable. per-call is a
// launch-captured limit whose runtime migration deletes the registry entry.
// The three limits moving on #9868, #9865, and #9836 are not in this tree;
// once their names appear here they must join ANALYSIS_SETTINGS.
// restart-only exception with a reason and an update_mode. A per-call limit
// belongs in ANALYSIS_SETTINGS. The three limits moving on #9868, #9865, and
// #9836 are not in this tree; once their names appear here they must join
// ANALYSIS_SETTINGS. REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES is numeric
// even though its name has no byte or time suffix.

const root = fileURLToPath(new URL("../../", import.meta.url));
const definitionsPath = join(root, "src/domain/analysisSettingDefinitions.ts");
Expand Down Expand Up @@ -72,23 +73,31 @@ const namesIn = (source, pattern) =>
const direct =
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(resolve(process.argv[1])).href;

if (direct) {
const failures = [];
const definitions = await readFile(definitionsPath, "utf8");
const registry = await readFile(registryPath, "utf8");
const runtime = namesIn(definitions, /environment:\s*"(REA_[A-Z0-9_]+)"/gu);
const declaredNames = [
...registry.matchAll(/name:\s*"(REA_[A-Z0-9_]+)"/gu),
].map((match) => match[1]);
const entries = [
...registry.matchAll(
/name:\s*"(REA_[A-Z0-9_]+)"\s*,\s*policy:\s*"(restart-only|per-call)"\s*,\s*read:\s*"(startup|per-call)"\s*,\s*reason:\s*"((?:\\.|[^"\\])*)"/gu,
/name:\s*"(REA_[A-Z0-9_]+)"\s*,\s*update_mode:\s*"(runtime|restart)"\s*,\s*policy:\s*"(restart-only|per-call)"\s*,\s*read:\s*"(startup|per-call)"\s*,\s*reason:\s*"((?:\\.|[^"\\])*)"/gu,
),
];
const listed = new Map();
for (const match of entries) {
const [, name, policy, read, reason] = match;
const [, name, updateMode, policy, read, reason] = match;
if (listed.has(name)) failures.push(`${name} is listed twice`);
listed.set(name, { policy, read, reason });
listed.set(name, { updateMode, policy, read, reason });
if (reason.trim().length < 40)
failures.push(`${name} needs a reason that states why`);
if (updateMode !== "restart")
failures.push(
`${name} is outside ANALYSIS_SETTINGS, so its update_mode must be restart`,
);
if (runtime.has(name))
failures.push(
`${name} is both a runtime setting and on the exception list`,
Expand All @@ -98,6 +107,19 @@ if (direct) {
if (policy === "restart-only" && read !== "startup")
failures.push(`${name} is restart-only but is not read at startup`);
}
for (const name of declaredNames) {
if (!listed.has(name))
failures.push(`${name} is registered without update_mode`);
}
const settingBodies = [
...definitions.matchAll(/\n {2}"[^"]+": \{([\s\S]*?)\n {2}\},/gu),
];
if (settingBodies.length === 0)
failures.push("ANALYSIS_SETTINGS did not parse into setting bodies");
for (const [, body] of settingBodies) {
if (!/updateMode:\s*"(?:runtime|restart)"/u.test(body))
failures.push("an analysis setting is missing updateMode");
}

const discovered = discoverNumericLimitUses(
await Promise.all(
Expand All @@ -121,6 +143,47 @@ if (direct) {
failures.push(`${name} is registered but no longer appears in src`);
}

const forbidden =
/(?<![A-Za-z0-9_])(?:rea_restart_required|restartRequired|restart_required)(?![A-Za-z0-9_])|effect:\s*"(?:immediate|next_process)"|SettingChangeEffect/gu;
const scanRoots = ["src", "tests", "docs", ".agents", "scripts"].map((dir) =>
join(root, dir),
Comment thread
morluto marked this conversation as resolved.
Resolved
);
const skipDir = new Set([
"node_modules",
"dist",
".turbo",
".cache",
"rea-numeric-limits.mjs",
]);
const listText = async (dir) => {
const output = [];
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return output;
}
for (const entry of entries) {
if (skipDir.has(entry.name)) continue;
const path = join(dir, entry.name);
if (entry.isDirectory()) output.push(...(await listText(path)));
else if (/\.(?:ts|tsx|mjs|js|md|json|py|yml|yaml)$/u.test(entry.name))
output.push(path);
}
return output;
};
for (const dir of scanRoots) {
for (const path of await listText(dir)) {
if (path.endsWith(`${sep}rea-numeric-limits.mjs`)) continue;
const source = await readFile(path, "utf8");
const hit = source.match(forbidden);
if (hit)
failures.push(
`${relative(root, path).split(sep).join("/")} still uses ${[...new Set(hit)].join(", ")}`,
);
}
}

if (failures.length > 0) {
console.error(`Numeric REA_ limit check failed:\n${failures.join("\n")}`);
process.exit(1);
Expand Down
3 changes: 2 additions & 1 deletion src/android/JadxOperationTimeout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ it("names REA_JADX_OPERATION_TIMEOUT_MS when the operation deadline elapses", ()
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
});
expect(projected).toMatchObject({
Expand Down
4 changes: 3 additions & 1 deletion src/application/AnalysisSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class AnalysisSettings {
};
}),
active_work_changed: false as const,
rea_restart_required: false as const,
pending_restart: false as const,
});
}
#describe(setting: AnalysisSettingId) {
Expand All @@ -156,6 +156,8 @@ export class AnalysisSettings {
minimum: definition.minimum,
maximum: definition.maximum,
applies_at: definition.appliesAt,
update_mode: definition.updateMode,
pending_restart: false as const,
provider: definition.provider,
operations: [...definition.operations],
...(definition.environment === undefined
Expand Down
3 changes: 2 additions & 1 deletion src/artifacts/ArtifactOperationFailure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ it("preserves adjustable reader admission facts and gives cleanup priority", ()
required_value: 2,
unit: "count",
applies_at: "next_operation",
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
},
});
Expand Down
3 changes: 2 additions & 1 deletion src/artifacts/ArtifactReaderLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export const artifactReaderLimitFailure = (
applies_at: "next_operation",
agent_editable: true,
can_increase: canIncrease,
rea_restart_required: false,
update_mode: "runtime" as const,
Comment thread
morluto marked this conversation as resolved.
Resolved
pending_restart: false as const,
},
});
};
3 changes: 2 additions & 1 deletion src/browser/history/HistoricalCaptureBudgets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ it.each(["max_input_bytes", "max_reply_bytes", "max_nesting_depth"] as const)(
parameter: control,
effective_value: selected,
source: "request",
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
},
});
Expand Down
6 changes: 3 additions & 3 deletions src/config/captureDecoderHeap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand All @@ -125,7 +124,8 @@ it("uses a valid REA_CAPTURE_DECODER_OLD_GENERATION_MIB override", async () => {
unit: "MiB",
applies_at: "next_worker",
agent_editable: true,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
reported_limits: {
old_generation_heap_mib: 256,
Expand Down
2 changes: 1 addition & 1 deletion src/config/captureDecoderHeap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(" ");
3 changes: 2 additions & 1 deletion src/config/debuggerSessionLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ it("names the session setting on a Rizin capacity refusal", async () => {
minimum: MINIMUM_DEBUGGER_MAX_SESSIONS,
maximum: MAXIMUM_DEBUGGER_MAX_SESSIONS,
unit: "sessions",
restart_required: true,
update_mode: "restart",
pending_restart: false,
max_sessions: 1,
active_or_reserved_sessions: 1,
},
Expand Down
38 changes: 33 additions & 5 deletions src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -229,10 +257,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,
Expand Down
6 changes: 3 additions & 3 deletions src/config/regexMatchingDeadline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ it("keeps the 5000 ms deadline when the setting is unset", () => {
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
reported_limits: {
matching_deadline_ms: DEFAULT_REGEX_MATCHING_DEADLINE.deadlineMs,
Expand All @@ -65,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,
);
Expand Down
2 changes: 1 addition & 1 deletion src/config/regexMatchingDeadline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(" ");
Loading
Loading