[GH-ISSUE #5965] [Bug] Historical capture reply-budget refusals use unreadable-result and doctor advice #3634

Closed
opened 2026-10-31 00:42:35 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5965

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

A historical-capture decoder's explicit reply-budget refusal is successfully parsed, but the shared service returns the generic explanation “Analysis returned an unreadable result” and recommends rea doctor. The specific sentence explaining the reply budget survives only in details.reason.

I executed the built shared service with a replacement format adapter that launched a real owned Node child and wrote the exact valid refusal emitted by the HAR/SAZ/qlog worker. The reply was:

{"ok":false,"reason":"limit","message":"Capture reply exceeds the complete-evidence output budget.","pointer":""}

A malformed-JSON control received the same generic unreadable-result/doctor wording. This report concerns the explanation and recovery advice. The existing AnalysisOutputError, unreadable_output code, and non-retryable status are explicitly allowed by the historical-capture contract and need not change.

Reproduction

In a source checkout with its locked dependencies and compiled runtime prepared, save the following as tests/process-global/captureReplyBudget.test.ts and run:

node node_modules/vitest/vitest.mjs run tests/process-global/captureReplyBudget.test.ts --maxWorkers 1

These two cases were executed within a four-case focused file. The unrelated cancellation cases and unused imports are omitted below; the file name is normalized. Both reply cases passed on the repository's pinned Node.js v24.18.0 and again on v24.22.0. The assertions characterize the current behavior. A separate real HAR decoder control succeeded.

import { access, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it } from "vitest";
import { HistoricalCaptureDecoder } from "../../dist/browser/history/HistoricalCaptureDecoder.js";
import { WebNetworkCaptureService } from "../../dist/application/WebNetworkCaptureService.js";
import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../dist/browser/history/CaptureRelease.js";
import { projectAnalysisError } from "../../dist/domain/analysisErrorProjection.js";
import { historicalHar } from "../fixtures/historicalHar.js";

it.each(["limit", "malformed"])("shared service projects decoder reply %s", async (scenario) => {
  const root = await mkdtemp(join(tmpdir(), "rea-reply-case-"));
  const capturePath = join(root, "capture.har");
  await writeFile(capturePath, JSON.stringify(historicalHar()));
  let runtimePath = "";
  const service = new WebNetworkCaptureService(new HistoricalCaptureDecoder([{
    format: "har", identity: HAR_CAPTURE_PROVIDER_IDENTITY,
    command: async (requestPath, runtime) => {
      runtimePath = runtime;
      return { command: process.execPath, arguments: ["-e", `
        const fs = require('node:fs');
        const request = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
        fs.writeFileSync(request.reply_path, ${JSON.stringify(scenario === "malformed" ? "invalid JSON" : JSON.stringify({ok: false, reason: "limit", message: "Capture reply exceeds the complete-evidence output budget.", pointer: ""}))}, {flag: 'wx', mode: 0o600});
      `, requestPath] };
    },
  }], process.env));
  try {
    const result = await service.inspect({capture_path: capturePath, format: "har"});
    if (result.ok) throw new Error("Expected reply refusal");
    const projected = projectAnalysisError(result.error);
    expect(projected).toMatchObject({code: "unreadable_output", category: "execution_failure", retryable: false});
    expect(projected.remediation.action).toContain("rea doctor");
    expect(projected.details?.reported_limits).toBeUndefined();
    if (scenario === "limit") expect(projected.details?.reason).toBe("Capture reply exceeds the complete-evidence output budget.");
    expect(result.error.cleanupIncomplete).toBe(false);
    await expect(access(runtimePath)).rejects.toMatchObject({code: "ENOENT"});
    console.log("REPLY", scenario, JSON.stringify(projected));
  } finally { await service.close(); await rm(root, {recursive: true, force: true}); }
});

The selected HAR is a small authored fixture. The adapter simulates the refusal protocol; it does not create a naturally oversized capture or execute native mitmproxy. The owned child, reply-file reading, JSON/schema parsing, shared service, canonical projection and private-root cleanup are real. Public CLI/MCP cancellation or delivery were not executed.

Expected behavior

Explain that the decoder refused because its complete serialized reply would exceed the fixed 96 MiB allowance, rather than diagnosing unreadable output or recommending installation health checks. Preserve the specific refusal, non-retryable status and current output-error classification.

The historical-capture resource contract distinguishes the reply allowance from input admission and process memory. It explicitly allows owned reply/output failures to remain output or provider errors. It also says record selection occurs after complete decoding. Recovery must therefore not suggest fewer selected records, MCP paging or additional worker memory as a way around this reply limit. A smaller capture exported by its producer is a different, narrower observation; it does not recover all of the refused Evidence.

The error class already has refusal-specific explanation and recovery options.

Impact and scope

P3 recovery-wording defect. The refusal remains safe and non-retryable, and the specific budget reason remains available in details. No incorrect successful analysis, data loss, security issue or persistent resource leak was observed. The injected refusal reproduced on both executed Node versions; natural occurrence frequency and actual downstream decisions are unknown.

Source tracing shows HAR, SAZ, qlog and native mitmproxy use this historical reply handler. Only a simulated HAR refusal and a real small HAR control were run. The CLI command and MCP registration call the shared service, but neither public adapter was exercised by this probe.

REA version

5241597594; package metadata reports rea-agents 6.7.0.

Runtime and platform

macOS 27.0.1 arm64; Node.js v24.18.0; Vitest 5.0.3. Identical focused cases also passed on Node.js v24.22.0.

Target and artifact context

Repository-authored HAR 1.2 fixture, with the worker's reply refusal supplied by a replacement format adapter. No browser connection, URL fetch, real oversized capture or native mitmproxy run.

Interface

Other

Earliest failure stage

Transport or result projection

Relevant output or evidence

Actual projection for the structured refusal:

{"code":"unreadable_output","category":"execution_failure","message":"Analysis returned an unreadable result. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed.","retryable":false,"remediation":{"action":"Analysis returned an unreadable result. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed."},"details":{"operation":"inspect_web_network_capture","reason":"Capture reply exceeds the complete-evidence output budget."}}

The test confirmed cleanupIncomplete === false and that the owned private runtime no longer existed. The existing classification test also passed when run alone: preserves limit decoder failures through the public error boundary in historicalCaptureDecoder.test.ts. That test explicitly expects AnalysisOutputError; this report does not request changing that expectation.

Typed outcome or recovery expected

Keep the intentional output refusal and retryable: false. Give it an accurate budget-specific message and remediation. State the effective 100663296-byte complete-reply limit and that this release provides no runtime override for this constant. Do not imply rea doctor, record selection, delivery mode or worker heap changes increase this allowance. No completed capture Evidence exists to retrieve from this refusal.

Analysis or suspected cause

Source-derived: the Node worker and native bridge explicitly emit the limit refusal after checking the serialized reply. The parent accepts that reply and constructs AnalysisOutputError without presentation options, so the generic fallback supplies unreadable-output and doctor advice.

The reply constant fixes the known allowance. Refusal-specific presentation at the owning branch would preserve the documented classification while distinguishing this case from malformed JSON. No repair was implemented or tested.

Additional context

Related #5848 concerns the separate EVM provider's reply refusal and classification. #5456 concerns SQLite recovery advice about nonexistent retained Evidence. #5631 lists other fixed capture producers; #5910 and #5808 track broader recovery-control work. None reports this historical decoder's generic unreadable-result/doctor explanation. Closed #2600 addressed expanded SAZ input admission and explicitly distinguished owned output failures.

Fetched upstream main at 25e1a24a3e. The relevant decoder, worker, native bridge, error class, fallback presentation and existing limit test are unchanged from the executed baseline. Later documentation edits concern API template prefix variance. No regression range or naturally oversized fixture was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5965 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior A historical-capture decoder's explicit reply-budget refusal is successfully parsed, but the shared service returns the generic explanation “Analysis returned an unreadable result” and recommends `rea doctor`. The specific sentence explaining the reply budget survives only in `details.reason`. I executed the built shared service with a replacement format adapter that launched a real owned Node child and wrote the exact valid refusal emitted by the HAR/SAZ/qlog worker. The reply was: ```json {"ok":false,"reason":"limit","message":"Capture reply exceeds the complete-evidence output budget.","pointer":""} ``` A malformed-JSON control received the same generic unreadable-result/doctor wording. This report concerns the explanation and recovery advice. The existing `AnalysisOutputError`, `unreadable_output` code, and non-retryable status are explicitly allowed by the historical-capture contract and need not change. ### Reproduction In a source checkout with its locked dependencies and compiled runtime prepared, save the following as `tests/process-global/captureReplyBudget.test.ts` and run: ```sh node node_modules/vitest/vitest.mjs run tests/process-global/captureReplyBudget.test.ts --maxWorkers 1 ``` These two cases were executed within a four-case focused file. The unrelated cancellation cases and unused imports are omitted below; the file name is normalized. Both reply cases passed on the repository's pinned Node.js v24.18.0 and again on v24.22.0. The assertions characterize the current behavior. A separate real HAR decoder control succeeded. ```ts import { access, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, it } from "vitest"; import { HistoricalCaptureDecoder } from "../../dist/browser/history/HistoricalCaptureDecoder.js"; import { WebNetworkCaptureService } from "../../dist/application/WebNetworkCaptureService.js"; import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../dist/browser/history/CaptureRelease.js"; import { projectAnalysisError } from "../../dist/domain/analysisErrorProjection.js"; import { historicalHar } from "../fixtures/historicalHar.js"; it.each(["limit", "malformed"])("shared service projects decoder reply %s", async (scenario) => { const root = await mkdtemp(join(tmpdir(), "rea-reply-case-")); const capturePath = join(root, "capture.har"); await writeFile(capturePath, JSON.stringify(historicalHar())); let runtimePath = ""; const service = new WebNetworkCaptureService(new HistoricalCaptureDecoder([{ format: "har", identity: HAR_CAPTURE_PROVIDER_IDENTITY, command: async (requestPath, runtime) => { runtimePath = runtime; return { command: process.execPath, arguments: ["-e", ` const fs = require('node:fs'); const request = JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); fs.writeFileSync(request.reply_path, ${JSON.stringify(scenario === "malformed" ? "invalid JSON" : JSON.stringify({ok: false, reason: "limit", message: "Capture reply exceeds the complete-evidence output budget.", pointer: ""}))}, {flag: 'wx', mode: 0o600}); `, requestPath] }; }, }], process.env)); try { const result = await service.inspect({capture_path: capturePath, format: "har"}); if (result.ok) throw new Error("Expected reply refusal"); const projected = projectAnalysisError(result.error); expect(projected).toMatchObject({code: "unreadable_output", category: "execution_failure", retryable: false}); expect(projected.remediation.action).toContain("rea doctor"); expect(projected.details?.reported_limits).toBeUndefined(); if (scenario === "limit") expect(projected.details?.reason).toBe("Capture reply exceeds the complete-evidence output budget."); expect(result.error.cleanupIncomplete).toBe(false); await expect(access(runtimePath)).rejects.toMatchObject({code: "ENOENT"}); console.log("REPLY", scenario, JSON.stringify(projected)); } finally { await service.close(); await rm(root, {recursive: true, force: true}); } }); ``` The selected HAR is a small authored fixture. The adapter simulates the refusal protocol; it does not create a naturally oversized capture or execute native mitmproxy. The owned child, reply-file reading, JSON/schema parsing, shared service, canonical projection and private-root cleanup are real. Public CLI/MCP cancellation or delivery were not executed. ### Expected behavior Explain that the decoder refused because its complete serialized reply would exceed the fixed 96 MiB allowance, rather than diagnosing unreadable output or recommending installation health checks. Preserve the specific refusal, non-retryable status and current output-error classification. The [historical-capture resource contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/web-network-captures.md#L600-L628) distinguishes the reply allowance from input admission and process memory. It explicitly allows owned reply/output failures to remain output or provider errors. It also says record selection occurs after complete decoding. Recovery must therefore not suggest fewer selected records, MCP paging or additional worker memory as a way around this reply limit. A smaller capture exported by its producer is a different, narrower observation; it does not recover all of the refused Evidence. The error class already has [refusal-specific explanation and recovery options](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorCore.ts#L96-L119). ### Impact and scope P3 recovery-wording defect. The refusal remains safe and non-retryable, and the specific budget reason remains available in details. No incorrect successful analysis, data loss, security issue or persistent resource leak was observed. The injected refusal reproduced on both executed Node versions; natural occurrence frequency and actual downstream decisions are unknown. Source tracing shows HAR, SAZ, qlog and native mitmproxy use this historical reply handler. Only a simulated HAR refusal and a real small HAR control were run. The [CLI command](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cli/webNetworkCaptureCommands.ts#L21-L85) and [MCP registration](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/server/registerWebNetworkCaptureTool.ts#L37-L68) call the shared service, but neither public adapter was exercised by this probe. ### REA version 52415975942a4b9c53a817915b313ccc404f280e; package metadata reports rea-agents 6.7.0. ### Runtime and platform macOS 27.0.1 arm64; Node.js v24.18.0; Vitest 5.0.3. Identical focused cases also passed on Node.js v24.22.0. ### Target and artifact context Repository-authored HAR 1.2 fixture, with the worker's reply refusal supplied by a replacement format adapter. No browser connection, URL fetch, real oversized capture or native mitmproxy run. ### Interface Other ### Earliest failure stage Transport or result projection ### Relevant output or evidence Actual projection for the structured refusal: ```json {"code":"unreadable_output","category":"execution_failure","message":"Analysis returned an unreadable result. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed.","retryable":false,"remediation":{"action":"Analysis returned an unreadable result. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed."},"details":{"operation":"inspect_web_network_capture","reason":"Capture reply exceeds the complete-evidence output budget."}} ``` The test confirmed `cleanupIncomplete === false` and that the owned private runtime no longer existed. The existing classification test also passed when run alone: `preserves limit decoder failures through the public error boundary` in [historicalCaptureDecoder.test.ts](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/tests/boundary/browser/historicalCaptureDecoder.test.ts#L562-L658). That test explicitly expects `AnalysisOutputError`; this report does not request changing that expectation. ### Typed outcome or recovery expected Keep the intentional output refusal and `retryable: false`. Give it an accurate budget-specific message and remediation. State the effective 100663296-byte complete-reply limit and that this release provides no runtime override for this constant. Do not imply `rea doctor`, record selection, delivery mode or worker heap changes increase this allowance. No completed capture Evidence exists to retrieve from this refusal. ### Analysis or suspected cause Source-derived: the [Node worker](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/browser/history/NodeCaptureProcess.ts#L59-L67) and [native bridge](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/bridge/mitmproxy/capture.py#L373-L379) explicitly emit the `limit` refusal after checking the serialized reply. The parent accepts that reply and [constructs `AnalysisOutputError` without presentation options](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/browser/history/HistoricalCaptureDecoder.ts#L215-L264), so the [generic fallback](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorPresentation.ts#L374-L376) supplies unreadable-output and doctor advice. The [reply constant](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/webNetworkCapture.ts#L11-L17) fixes the known allowance. Refusal-specific presentation at the owning branch would preserve the documented classification while distinguishing this case from malformed JSON. No repair was implemented or tested. ### Additional context Related #5848 concerns the separate EVM provider's reply refusal and classification. #5456 concerns SQLite recovery advice about nonexistent retained Evidence. #5631 lists other fixed capture producers; #5910 and #5808 track broader recovery-control work. None reports this historical decoder's generic unreadable-result/doctor explanation. Closed #2600 addressed expanded SAZ input admission and explicitly distinguished owned output failures. Fetched upstream main at 25e1a24a3ed3671a0148757555f18d16c29d936a. The relevant decoder, worker, native bridge, error class, fallback presentation and existing limit test are unchanged from the executed baseline. Later documentation edits concern API template prefix variance. No regression range or naturally oversized fixture was established.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

The explicit decoder limit reply now constructs AnalysisOutputError with a budget-specific userMessage, a 100663296-byte no-override remediation, and structured reply-boundary diagnostics. It explicitly rejects record selection/delivery/worker-memory as ways to increase this allowance and states no completed Evidence exists. The reason=limit regression checks the output class, retryability, limit wording and absence of doctor advice.

Implementation: HistoricalCaptureDecoder.ts.

The relevant regression checks in historicalCaptureDecoder.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380730996 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. The explicit decoder limit reply now constructs AnalysisOutputError with a budget-specific userMessage, a 100663296-byte no-override remediation, and structured reply-boundary diagnostics. It explicitly rejects record selection/delivery/worker-memory as ways to increase this allowance and states no completed Evidence exists. The reason=limit regression checks the output class, retryability, limit wording and absence of doctor advice. Implementation: [HistoricalCaptureDecoder.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/browser/history/HistoricalCaptureDecoder.ts#L259). The relevant regression checks in [historicalCaptureDecoder.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/browser/historicalCaptureDecoder.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3634
No description provided.