[GH-ISSUE #4683] [Bug] Late SIGINT discards completed EVM interface Evidence during successful cleanup #2442

Closed
opened 2026-10-17 21:04:42 +00:00 by bsakaguchi · 4 comments
Owner

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

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

inspect-evm-interface discards its completed interface result when SIGINT arrives during awaited private-root cleanup. On both tested builds, three out of three deliberately timed interruptions returned cancelled with cleanup: complete and captured output, but no details.partial_observation. Each build's noninterrupted control and subsequent retry returned complete Evidence for the same selected bytes. Every case removed its runtime root.

This is a Linux x64 public-dispatcher reproduction with the real bundled EVMole worker. A preload hook controls only the timing of root removal: it sends a real SIGINT when the EVM root's removal is requested, waits 50 ms to let the signal handler run, then performs the original filesystem removal. No worker reply, provider result, abort signal, or error payload is fabricated. These counts describe this controlled timing seam; natural occurrence frequency was not measured.

Reproduction

Prepare the main source checkout using its pinned toolchain, npm ci, and npm run build:cached. For the published package comparison, install with npm install --prefix ./release rea-agents@6.5.0 and use release/node_modules/rea-agents as the package root. Run on Linux x64 with executable util-linux prlimit on PATH. Set REA_ROOT to the absolute package root and NODE to the absolute path of the supported Node executable before entering a disposable working directory.

Create the authored fixture and this cleanup-hook.mjs:

printf '0x60006000f3\n' > selected.hex
import fs from 'node:fs/promises';
import { basename } from 'node:path';
import { syncBuiltinESMExports } from 'node:module';
import { setTimeout as delay } from 'node:timers/promises';

const remove = fs.rm;
fs.rm = async function (path, options) {
  if (!basename(String(path)).startsWith('rea-evm-interface-')) {
    return remove.call(this, path, options);
  }
  process.stderr.write('cleanup-enter\n');
  if (process.env.INTERRUPT_CLEANUP === '1') {
    process.kill(process.pid, 'SIGINT');
    await delay(50);
  }
  await remove.call(this, path, options);
  try {
    await fs.access(path);
    throw new Error('runtime root still exists');
  } catch (error) {
    if (error.code !== 'ENOENT') throw error;
  }
  process.stderr.write('cleanup-removed\n');
};
syncBuiltinESMExports();

The runner invoked the command below in a new home and temporary directory for each case, using INTERRUPT_CLEANUP=0 for the control and retry and 1 for each of three interrupted attempts. The following shell loop expresses those same invocations; the measured run used a Python subprocess runner with captured stdout/stderr and a 60-second timeout per invocation.

for mode in control interrupt-1 interrupt-2 interrupt-3 retry; do
  mkdir -p "$PWD/$mode/home" "$PWD/$mode/tmp"
  interrupt=0
  case "$mode" in interrupt-*) interrupt=1 ;; esac
  env -i PATH="$(dirname "$NODE"):/usr/bin:/bin" \
    HOME="$PWD/$mode/home" TMPDIR="$PWD/$mode/tmp" \
    INTERRUPT_CLEANUP="$interrupt" \
    "$NODE" --import "$PWD/cleanup-hook.mjs" \
    "$REA_ROOT/scripts/rea.mjs" inspect-evm-interface \
    "$PWD/selected.hex" hex --json \
    > "$mode/stdout.json" 2> "$mode/stderr.txt"
  printf '%s exit=%s\n' "$mode" "$?"
done

Repeat in a separate disposable directory for the other package root. All stderr files contained cleanup-enter followed by cleanup-removed. The four successful responses retained normalized_result.bytecode.hex: "60006000f3", byte count 5, SHA-256 10a958938acc6e15066f1e3d64f4871b196412a67d59ab1664f5d38f406206b6, metadata status unavailable, empty inferred functions, and raw producer output. The six interrupted responses omitted partial Evidence entirely.

Expected behavior

Retain the validated completed interface facts as partial Evidence under typed cancellation, while still reporting actual cleanup status. The current lifecycle contract requires carrying cancellation through awaited cleanup and retaining collected observations; its regression guidance specifically covers cancellation during temporary-root removal. The release changelog also promises retention of completed Evidence and partial observations when analysis or cleanup fails.

Impact and scope

The interrupted response loses already-computed bytecode identity, metadata, inferred function candidates and raw producer output. The failure window begins after successful result validation and ends before provider return. The authored fixture has no function candidates; larger contract inputs and MCP cancellation delivery were not runtime-tested. Input bytes are not modified, and successful retries recovered complete Evidence.

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A bounded late-interruption case discards completed inspection facts on both tested builds; the same input succeeds on retry, with no persistent input loss observed. Natural timing frequency is unknown.
  • Workaround status: available
  • Workaround basis: Retrying the same offline inspection without SIGINT succeeded once on each build after the three interrupted attempts and returned complete Evidence for the same selected bytes.

REA version

Published npm package rea-agents@6.5.0; main source build at 1e36e707b1f991fde826947bab04d436e64b7f97. A later main freshness check at 556504edcbb0f8cd9eaf81a91b18ff891b8f5457 found no changes to the provider, service, root cleanup, CLI command/finalizer, or cited lifecycle documentation; that later revision was not separately built.

Runtime and platform

Node v24.18.0, npm 11.16.0; Debian GNU/Linux 13 (trixie), Linux 6.12.94+, x86_64; util-linux prlimit 2.41.5. Main used locked dependencies; the package used its published bundled runtime. Each invocation used an isolated HOME and TMPDIR and an environment containing only those paths, PATH and the timing-hook switch.

Target and artifact context

Authored flat ASCII hex carrier containing 0x60006000f3 followed by LF. Offline interface inference only; the target bytecode is not executed, and no chain query or transaction is involved.

Interface

CLI

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

  • Expected source: Current docs/tool-design.md:93-100 requires retained observations under typed cancellation; docs/testing.md:214-237 names cancellation during root removal and independent provider/shared-workflow checks. CHANGELOG.md:12 states the release's completed-Evidence retention guarantee.
  • Failure source: Six public CLI outcomes obtained with the cleanup timing hook above, paired with four successful control/retry outcomes. src/evm/EvmoleInterfaceProvider.ts:447-455 assigns the validated successful execution; :556-584 awaits root cleanup and replaces that execution with a cancellation error containing captured output only. src/application/evm/EvmInterfaceService.ts:57-58 returns that provider error before Evidence assembly.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Each interrupted run exited 130 with these relevant fields:

{
  "code": "cancelled",
  "category": "cancelled",
  "details": {
    "operation": "inspect_evm_interface",
    "cleanup": "complete",
    "captured_output": { "stdout": "", "stderr": "", "truncated": false }
  }
}

partial_observation is absent. The cleanup hook independently checked ENOENT after real removal, and the runner found no remaining entries in each case's temporary directory. Source comparison with the release tag found the same final provider guard; main's only provider change since that release concerns an earlier error branch.

Typed outcome or recovery expected

The same timed interruption should still exit as cancelled and report complete cleanup, while retaining completed Evidence under details.partial_observation, including normalized_result, carrier/decoded-byte identity, function candidates, metadata, raw_result, provenance and original limitations. The identical noninterrupted control must remain successful. Existing primary failures and cleanup failures must keep their classifications. Verify actual root removal; merely suppressing cancellation or returning ordinary success would not restore this contract.

Analysis or suspected cause

Source-derived: the provider's final guard duplicates late-cancellation classification but constructs a thinner error before the service can build Evidence. Its cleanup-failure path already constructs partial Evidence from the same successful execution at src/evm/EvmoleInterfaceProvider.ts:120-147; the successful-cleanup cancellation path does not. The asynchronous root-removal wait permits the signal handler to run after validation and assignment of the successful result.

Additional context

Issue #4261 expanded to EvmInterfaceService and repaired successful provider returns at the service boundary. Its fix, commit 3b698645a, changed the service and its controlled-provider tests, without changing this provider guard. The production provider instead returns an error before that repaired path can retain its facts. This report concerns the remaining provider boundary.

Issue #4254 fixes WASM completion and retains its Evidence after final cleanup. Issue #4640 concerns Flutter success after interruption during a later cleanup retry. Neither describes this EVM provider's cancellation-without-completed-Evidence outcome. No delivery to an already-cancelled MCP client is claimed.

Originally created by @coygeek on GitHub (Oct 17, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4683 ### 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 `inspect-evm-interface` discards its completed interface result when SIGINT arrives during awaited private-root cleanup. On both tested builds, three out of three deliberately timed interruptions returned `cancelled` with `cleanup: complete` and captured output, but no `details.partial_observation`. Each build's noninterrupted control and subsequent retry returned complete Evidence for the same selected bytes. Every case removed its runtime root. This is a Linux x64 public-dispatcher reproduction with the real bundled EVMole worker. A preload hook controls only the timing of root removal: it sends a real SIGINT when the EVM root's removal is requested, waits 50 ms to let the signal handler run, then performs the original filesystem removal. No worker reply, provider result, abort signal, or error payload is fabricated. These counts describe this controlled timing seam; natural occurrence frequency was not measured. ### Reproduction Prepare the main source checkout using its pinned toolchain, `npm ci`, and `npm run build:cached`. For the published package comparison, install with `npm install --prefix ./release rea-agents@6.5.0` and use `release/node_modules/rea-agents` as the package root. Run on Linux x64 with executable util-linux `prlimit` on PATH. Set `REA_ROOT` to the absolute package root and `NODE` to the absolute path of the supported Node executable before entering a disposable working directory. Create the authored fixture and this `cleanup-hook.mjs`: ```sh printf '0x60006000f3\n' > selected.hex ``` ```js import fs from 'node:fs/promises'; import { basename } from 'node:path'; import { syncBuiltinESMExports } from 'node:module'; import { setTimeout as delay } from 'node:timers/promises'; const remove = fs.rm; fs.rm = async function (path, options) { if (!basename(String(path)).startsWith('rea-evm-interface-')) { return remove.call(this, path, options); } process.stderr.write('cleanup-enter\n'); if (process.env.INTERRUPT_CLEANUP === '1') { process.kill(process.pid, 'SIGINT'); await delay(50); } await remove.call(this, path, options); try { await fs.access(path); throw new Error('runtime root still exists'); } catch (error) { if (error.code !== 'ENOENT') throw error; } process.stderr.write('cleanup-removed\n'); }; syncBuiltinESMExports(); ``` The runner invoked the command below in a new home and temporary directory for each case, using `INTERRUPT_CLEANUP=0` for the control and retry and `1` for each of three interrupted attempts. The following shell loop expresses those same invocations; the measured run used a Python subprocess runner with captured stdout/stderr and a 60-second timeout per invocation. ```sh for mode in control interrupt-1 interrupt-2 interrupt-3 retry; do mkdir -p "$PWD/$mode/home" "$PWD/$mode/tmp" interrupt=0 case "$mode" in interrupt-*) interrupt=1 ;; esac env -i PATH="$(dirname "$NODE"):/usr/bin:/bin" \ HOME="$PWD/$mode/home" TMPDIR="$PWD/$mode/tmp" \ INTERRUPT_CLEANUP="$interrupt" \ "$NODE" --import "$PWD/cleanup-hook.mjs" \ "$REA_ROOT/scripts/rea.mjs" inspect-evm-interface \ "$PWD/selected.hex" hex --json \ > "$mode/stdout.json" 2> "$mode/stderr.txt" printf '%s exit=%s\n' "$mode" "$?" done ``` Repeat in a separate disposable directory for the other package root. All stderr files contained `cleanup-enter` followed by `cleanup-removed`. The four successful responses retained `normalized_result.bytecode.hex: "60006000f3"`, byte count 5, SHA-256 `10a958938acc6e15066f1e3d64f4871b196412a67d59ab1664f5d38f406206b6`, metadata status `unavailable`, empty inferred functions, and raw producer output. The six interrupted responses omitted partial Evidence entirely. ### Expected behavior Retain the validated completed interface facts as partial Evidence under typed cancellation, while still reporting actual cleanup status. The current lifecycle contract requires carrying cancellation through awaited cleanup and retaining collected observations; its regression guidance specifically covers cancellation during temporary-root removal. The release changelog also promises retention of completed Evidence and partial observations when analysis or cleanup fails. ### Impact and scope The interrupted response loses already-computed bytecode identity, metadata, inferred function candidates and raw producer output. The failure window begins after successful result validation and ends before provider return. The authored fixture has no function candidates; larger contract inputs and MCP cancellation delivery were not runtime-tested. Input bytes are not modified, and successful retries recovered complete Evidence. - Impact level: P2 - Assessment status: supported - Impact basis: A bounded late-interruption case discards completed inspection facts on both tested builds; the same input succeeds on retry, with no persistent input loss observed. Natural timing frequency is unknown. - Workaround status: available - Workaround basis: Retrying the same offline inspection without SIGINT succeeded once on each build after the three interrupted attempts and returned complete Evidence for the same selected bytes. ### REA version Published npm package `rea-agents@6.5.0`; main source build at `1e36e707b1f991fde826947bab04d436e64b7f97`. A later main freshness check at `556504edcbb0f8cd9eaf81a91b18ff891b8f5457` found no changes to the provider, service, root cleanup, CLI command/finalizer, or cited lifecycle documentation; that later revision was not separately built. ### Runtime and platform Node v24.18.0, npm 11.16.0; Debian GNU/Linux 13 (trixie), Linux 6.12.94+, x86_64; util-linux prlimit 2.41.5. Main used locked dependencies; the package used its published bundled runtime. Each invocation used an isolated HOME and TMPDIR and an environment containing only those paths, PATH and the timing-hook switch. ### Target and artifact context Authored flat ASCII hex carrier containing `0x60006000f3` followed by LF. Offline interface inference only; the target bytecode is not executed, and no chain query or transaction is involved. ### Interface CLI ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence - Expected source: Current `docs/tool-design.md:93-100` requires retained observations under typed cancellation; `docs/testing.md:214-237` names cancellation during root removal and independent provider/shared-workflow checks. `CHANGELOG.md:12` states the release's completed-Evidence retention guarantee. - Failure source: Six public CLI outcomes obtained with the cleanup timing hook above, paired with four successful control/retry outcomes. `src/evm/EvmoleInterfaceProvider.ts:447-455` assigns the validated successful execution; `:556-584` awaits root cleanup and replaces that execution with a cancellation error containing captured output only. `src/application/evm/EvmInterfaceService.ts:57-58` returns that provider error before Evidence assembly. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete Each interrupted run exited 130 with these relevant fields: ```json { "code": "cancelled", "category": "cancelled", "details": { "operation": "inspect_evm_interface", "cleanup": "complete", "captured_output": { "stdout": "", "stderr": "", "truncated": false } } } ``` `partial_observation` is absent. The cleanup hook independently checked `ENOENT` after real removal, and the runner found no remaining entries in each case's temporary directory. Source comparison with the release tag found the same final provider guard; main's only provider change since that release concerns an earlier error branch. ### Typed outcome or recovery expected The same timed interruption should still exit as cancelled and report complete cleanup, while retaining completed Evidence under `details.partial_observation`, including `normalized_result`, carrier/decoded-byte identity, function candidates, metadata, `raw_result`, provenance and original limitations. The identical noninterrupted control must remain successful. Existing primary failures and cleanup failures must keep their classifications. Verify actual root removal; merely suppressing cancellation or returning ordinary success would not restore this contract. ### Analysis or suspected cause Source-derived: the provider's final guard duplicates late-cancellation classification but constructs a thinner error before the service can build Evidence. Its cleanup-failure path already constructs partial Evidence from the same successful execution at `src/evm/EvmoleInterfaceProvider.ts:120-147`; the successful-cleanup cancellation path does not. The asynchronous root-removal wait permits the signal handler to run after validation and assignment of the successful result. ### Additional context [Issue #4261](https://github.com/morluto/rea/issues/4261) expanded to EvmInterfaceService and repaired successful provider returns at the service boundary. Its fix, commit `3b698645a`, changed the service and its controlled-provider tests, without changing this provider guard. The production provider instead returns an error before that repaired path can retain its facts. This report concerns the remaining provider boundary. [Issue #4254](https://github.com/morluto/rea/issues/4254) fixes WASM completion and retains its Evidence after final cleanup. [Issue #4640](https://github.com/morluto/rea/issues/4640) concerns Flutter success after interruption during a later cleanup retry. Neither describes this EVM provider's cancellation-without-completed-Evidence outcome. No delivery to an already-cancelled MCP client is claimed.
Author
Owner

@morluto commented on GitHub (Oct 17, 2026):

Source repair published in d694aef227 on main.

The provider's successful-cleanup cancellation guard now retains full completed Evidence and the exact caller reason. Success, cleanup failure and late cancellation use one shared Evidence assembler, preserving selected carrier identity/encoding, decoded bytes, raw producer output, locations, inference confidence and limitations. Provider/service admission also preserve opaque cancellation reasons.

Executed on Darwin arm64: 26 service, official MCP and host-refusal checks passed, with 30 Linux x64 provider checks explicitly skipped. Expanded Linux root-removal controls cover success, late cancellation, cleanup failure and cancellation plus cleanup failure; they retain captured warnings, assert partial Evidence and reason identity, verify real root removal and check service forwarding. Those Linux controls were not executed here. Cached Linux images were arm64, so no host substitution or package installation was used. check:fast, docs:check including five README checks, and normal commit/pre-push hooks passed.

The real Linux x64 EVMole worker and the report's public SIGINT timing reproduction remain unverified. No natural occurrence rate, performance, token or peak-memory measurements are claimed. The issue remains open for that supported-host qualification. Separate adjacent command-reason loss was confirmed at the compiled failure boundary and filed as #4686; repaired separately in 0ec4826633.

<!-- gh-comment-id:6230083540 --> @morluto commented on GitHub (Oct 17, 2026): Source repair published in d694aef227e90164e6654ea9e5ce6a854d099404 on main. The provider's successful-cleanup cancellation guard now retains full completed Evidence and the exact caller reason. Success, cleanup failure and late cancellation use one shared Evidence assembler, preserving selected carrier identity/encoding, decoded bytes, raw producer output, locations, inference confidence and limitations. Provider/service admission also preserve opaque cancellation reasons. Executed on Darwin arm64: 26 service, official MCP and host-refusal checks passed, with 30 Linux x64 provider checks explicitly skipped. Expanded Linux root-removal controls cover success, late cancellation, cleanup failure and cancellation plus cleanup failure; they retain captured warnings, assert partial Evidence and reason identity, verify real root removal and check service forwarding. **Those Linux controls were not executed here.** Cached Linux images were arm64, so no host substitution or package installation was used. `check:fast`, `docs:check` including five README checks, and normal commit/pre-push hooks passed. The real Linux x64 EVMole worker and the report's public SIGINT timing reproduction remain unverified. No natural occurrence rate, performance, token or peak-memory measurements are claimed. The issue remains open for that supported-host qualification. Separate adjacent command-reason loss was confirmed at the compiled failure boundary and filed as #4686; repaired separately in 0ec4826633482be4ac3bbc194cc677f50db72a87.
Author
Owner

@XinwangTao commented on GitHub (Oct 18, 2026):

Confirmed the same class of issue I hit in the snapshot cleanup path (#4734): a cancellation signal that arrives while awaited private-root cleanup is in flight ends up swallowing the already-completed EVM interface Evidence. The result is especially confusing because the return is cancelled with cleanup: complete and captured output yet the partial_observation is missing, so consumers see a mixed signal that neither says "cancelled before work" nor "work done". Since your controlled repro shows 3/3 interruptions reproduce it with the real EVMole worker, I'd suggest that the cancellation handling distinguish between "cleanup hasn't started" and "cleanup in progress over completed output" — the latter should preserve the completed Evidence and surface the interruption only as an auxiliary signal, mirroring how you retain ownership through cleanup elsewhere. A regression test asserting that a SIGINT during root removal still returns complete Evidence with details.partial_observation would lock this in.

<!-- gh-comment-id:6248365765 --> @XinwangTao commented on GitHub (Oct 18, 2026): Confirmed the same class of issue I hit in the snapshot cleanup path (#4734): a cancellation signal that arrives while awaited private-root cleanup is in flight ends up swallowing the already-completed EVM interface Evidence. The result is especially confusing because the return is `cancelled with cleanup: complete and captured output` yet the partial_observation is missing, so consumers see a mixed signal that neither says "cancelled before work" nor "work done". Since your controlled repro shows 3/3 interruptions reproduce it with the real EVMole worker, I'd suggest that the cancellation handling distinguish between "cleanup hasn't started" and "cleanup in progress over completed output" — the latter should preserve the completed Evidence and surface the interruption only as an auxiliary signal, mirroring how you retain ownership through cleanup elsewhere. A regression test asserting that a SIGINT during root removal still returns complete Evidence with details.partial_observation would lock this in.
Author
Owner

@CassieFox23 commented on GitHub (Oct 18, 2026):

Checked on Linux: this is fixed on main by d694aef22. The commit's message says "Refs #4683", which is why GitHub left this issue open.

  • src/application/evm/EvmInterfaceService.test.ts and tests/boundary/evm/evmoleInterfaceProvider.test.ts pass on main 36d156ff0 on Node 22.23.1, 24.18.0 and 26.11.1 (43 tests, 2 skipped).
  • With the fix reverted on main, 4 tests fail.

Thanks, coygeek, for the clear report.

If nothing else is open here, this can be closed.

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6254870610 --> @CassieFox23 commented on GitHub (Oct 18, 2026): Checked on Linux: this is fixed on main by d694aef22. The commit's message says "Refs #4683", which is why GitHub left this issue open. - `src/application/evm/EvmInterfaceService.test.ts` and `tests/boundary/evm/evmoleInterfaceProvider.test.ts` pass on main 36d156ff0 on Node 22.23.1, 24.18.0 and 26.11.1 (43 tests, 2 skipped). - With the fix reverted on main, 4 tests fail. Thanks, coygeek, for the clear report. If nothing else is open here, this can be closed. 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
Author
Owner

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

Closing as fixed by d694aef, included in rea-agents 6.7.0.

Thanks @morluto for the repair and @CassieFox23 for the Linux regression verification: 43 tests passed, 2 skipped, and 4 failures when the fix was reverted. Closing based on the released repair and those reported regression results.

<!-- gh-comment-id:6255695929 --> @coygeek commented on GitHub (Oct 18, 2026): Closing as fixed by [d694aef](https://github.com/morluto/rea/commit/d694aef227e90164e6654ea9e5ce6a854d099404), included in rea-agents 6.7.0. Thanks @morluto for the repair and @CassieFox23 for the [Linux regression verification](https://github.com/morluto/rea/issues/4683#issuecomment-6254870610): 43 tests passed, 2 skipped, and 4 failures when the fix was reverted. Closing based on the released repair and those reported regression results.
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#2442
No description provided.