[GH-ISSUE #4614] [Bug] IDA startup failures identify internal operations instead of the requested analysis #2399

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

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

Before submitting

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

Area

Native analysis or decompilation

Observed behavior

The public CLI command rea decompile <binary> main --provider ida --json returns a startup failure naming an internal operation instead of the requested procedure_pseudo_code. On both tested builds, an IDA headless profile missing list_funcs produces execution_failure with details.operation: "initialize".

On the source build, stalled tool-catalog discovery and initialization also produce typed timeouts identifying ida_mcp_catalog and ida_mcp_connect. Those two identity failures do not reproduce on the published package, which returns execution_failure under the requested operation instead. All startup cases fail before dispatching a provider tool call.

The fixture also exercises busy-health waits as context. Both builds retain their documented busy-tool operation text. A follow-up health timeout preserves the completed decompiler reply as partial Evidence under procedure_pseudo_code, with target binding explicitly unverified. This report does not classify the documented health-timeout representation as a bug or request a change to it. All observations use a simulated loopback MCP provider.

Reproduction

  1. For the source build, check out the revision listed below, use its pinned Node/npm toolchain, run npm ci and npm run build:cached, and run this fixture from the checkout root. For the published package, run npm install --prefix release --ignore-scripts --no-audit --no-fund rea-agents@6.5.0, change directory to release/node_modules/rea-agents, and run the same fixture. Both roots contain scripts/rea.mjs. Run it twice per build. Each CLI child receives a fresh HOME and TMPDIR and a whitelist environment. No IDA installation or external service is used.
node --input-type=module <<'NODE'
import { createServer } from 'node:http';
import { spawn } from 'node:child_process';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import assert from 'node:assert/strict';
const root = await mkdtemp(join(tmpdir(), 'rea-ida-repro-'));
const tools = ['idb_open','idb_list','idb_close','server_health','list_funcs','lookup_funcs','find_regex','decompile','disasm','callees','xrefs_to'];
try {
 const target = join(root,'target.elf');
 const bytes = Buffer.alloc(64); bytes.set([127,69,76,70,2,1,1]); bytes.writeUInt16LE(62,18); bytes.writeUInt32LE(1,20); bytes.writeUInt16LE(64,52); await writeFile(target,bytes);
 for (const mode of ['pre','post','startup','catalog-timeout','connect-timeout','catalog','success']) {
  let database, input, health=0, primary=false; const calls=[];
  const server=createServer(async(req,res)=>{
   if(req.method!=='POST'){res.writeHead(405).end();return;}
   let body='';for await(const chunk of req)body+=chunk;
   const m=JSON.parse(body);if(m.id===undefined){res.writeHead(202).end();return;}
   const send=result=>res.writeHead(200,{'Content-Type':'application/json'}).end(JSON.stringify({jsonrpc:'2.0',id:m.id,result}));
   if(m.method==='initialize'){if(mode==='connect-timeout')return;send({protocolVersion:m.params.protocolVersion,capabilities:{tools:{}},serverInfo:{name:'ida-fixture',version:'1'}});return;}
   if(m.method==='tools/list') {if(mode==='catalog-timeout')return;if(mode==='catalog'){res.writeHead(200,{'Content-Type':'application/json'}).end(JSON.stringify({jsonrpc:'2.0',id:m.id,error:{code:-32603,message:'fixture catalog refused'}}));return;} send({tools:tools.filter(n=>mode!=='startup'||n!=='list_funcs').map(name=>({name,inputSchema:{type:'object',properties:{}}}))});return;}
   const {name,arguments:a}=m.params;calls.push(name);let p;
   if(name==='idb_open'){database=a.preferred_session_id;input=a.input_path;p={success:true,session:{session_id:database,input_path:input}};}
   else if(name==='idb_list')p={sessions:database?[{session_id:database,input_path:input,owned:true,backend:'worker',is_active:true}]:[]};
   else if(name==='idb_close'){p={success:true,session_id:database,saved:null,owned:true,backend:'worker'};database=undefined;}
   else if(name==='server_health'){health++;p=(mode==='pre'&&health>=1)||(mode==='post'&&primary)?{status:'busy',busy_tool:'config_json_get',busy_sec:0}:{status:'ok',input_path:input,idb_path:input+'.i64',module:'target.elf',imagebase:'0x1000',auto_analysis_ready:true};}
   else if(name==='lookup_funcs')p=a.queries.map(query=>({query,fn:{addr:'0x1000',name:'main',size:'0x20'},error:null}));
   else if(name==='decompile'){primary=true;p={code:'int main(void) { return 42; }',cursor:{done:true}};}
   else throw Error(name);
   send({content:[{type:'text',text:JSON.stringify(p)}]});
  });
  await new Promise(r=>server.listen(0,'127.0.0.1',r));
  try {
   const home=await mkdtemp(join(root,'home-')); const work=join(home,'work');await mkdir(work);
   const config=join(home,'ida.json');await writeFile(config,JSON.stringify({url:`http://127.0.0.1:${server.address().port}/mcp`,mode:'headless',timeoutMs:300,workspaceRoot:work}));
   const child=spawn(process.execPath,['scripts/rea.mjs','decompile',target,'main','--provider','ida','--json'],{env:{PATH:process.env.PATH,HOME:home,TMPDIR:home,REA_IDA_MCP_CONFIG:config,REA_LOG_LEVEL:'silent'}});
   let stdout='',stderr='';child.stdout.on('data',x=>stdout+=x);child.stderr.on('data',x=>stderr+=x);
   const code=await new Promise((resolve,reject)=>{child.on('error',reject);child.on('close',resolve)});
   let o;try{o=JSON.parse(stdout);}catch{console.log({mode,code,stdout,stderr});throw Error('not JSON');}
   assert.equal(stderr,'');
   assert.equal(database,undefined);
   const summary={mode,code,calls,error:o.code,operation:o.details?.operation??o.operation,retryable:o.retryable,timeout:o.details?.timeout_ms,missing:o.details?.diagnostics?.missing_tools,partialOperation:o.details?.partial_observation?.operation,failedOperation:o.details?.partial_observation?.normalized_result?.failed_operation};
   console.log(JSON.stringify(summary));
   if(process.env.REA_REPRO_OUTPUT)await writeFile(join(process.env.REA_REPRO_OUTPUT,mode+'.json'),JSON.stringify(o,null,2));
   if(mode==='success'){assert.equal(code,0);assert.equal(o.operation,'procedure_pseudo_code');}
   else {assert.equal(code,1);assert.equal(summary.operation,mode==='startup'?'initialize':mode==='catalog'?'procedure_pseudo_code':mode==='catalog-timeout'?(o.code==='provider_timeout'?'ida_mcp_catalog':'procedure_pseudo_code'):mode==='connect-timeout'?(o.code==='provider_timeout'?'ida_mcp_connect':'procedure_pseudo_code'):'server_health: headless worker still busy in config_json_get');if(mode==='pre'||mode==='post'){assert.equal(o.code,'provider_timeout');assert.equal(o.retryable,true);assert.equal(summary.timeout,300);}if(mode==='pre'){assert(!calls.includes('lookup_funcs'));assert(!calls.includes('decompile'));assert.equal(o.details?.partial_observation,undefined);}if(mode==='catalog-timeout'||mode==='connect-timeout'){if(o.code==='provider_timeout'){assert.equal(o.retryable,true);assert.equal(summary.timeout,300);}else{assert.equal(o.code,'execution_failure');assert.equal(o.retryable,false);}}if(mode==='startup'){assert.equal(o.code,'execution_failure');assert.deepEqual(summary.missing,['list_funcs']);}if(mode==='post'){assert(calls.includes('decompile'));assert.equal(o.details.partial_observation.normalized_result.target_binding.status,'not_verified');assert.equal(o.details.partial_observation.raw_result.post_operation_identity_check,'started');assert.equal(o.details.partial_observation.raw_result.observations.at(-1).result.code,'int main(void) { return 42; }');assert.equal(summary.partialOperation,'procedure_pseudo_code');assert.equal(summary.failedOperation,'procedure_pseudo_code');}}
  } finally {server.closeAllConnections();await new Promise(r=>server.close(r));}
 }
} finally {await rm(root,{recursive:true,force:true});}
NODE
  1. Read startup, which omits only list_funcs from the advertised profile. It returns execution_failure naming initialize. The catalog and handshake timeout rows identify internal operations on the source build. The published package instead returns a generic execution_failure under the correct requested operation for those two timeout rows.

  2. Compare the catalog RPC-refusal and success controls, which identify the requested operation on both builds. The pre and post health rows provide contextual observations of the documented busy-tool diagnostic. The first runs no lookup or decompile; the second retains the completed reply as partial Evidence. The fixture confirms that owned workers were closed.

  3. All seven rows completed twice per build. The fixture asserts current outputs, including the known build difference for startup timeouts. When checking restoration, change the startup expectation and the source build's two startup-timeout identity expectations to the caller's operation. Retain the health, category, diagnostic, partial-result and cleanup controls.

Expected behavior

A startup failure returned for the accepted analysis request should retain its caller operation, procedure_pseudo_code, while preserving auxiliary-stage diagnostics, missing tools, retryability, actual timeout budget, captured output and cleanup facts. The MCP contract says provider tools require their selected analysis runtime and that these failures are reported at the operation that needs it. The accepted analysis request triggers lazy IDA startup and needs that runtime.

Fail closed on an incompatible profile or stalled initialization. Keep the startup failure classification, actual request budget and missing-tool diagnostics. The ordinary catalog-refusal control already reports the accepted analysis operation. Preserve the independently documented busy-health behavior and its retained partial Evidence.

Impact and scope

The observed defect changes a machine-readable operation identity during IDA startup. Callers inspecting details.operation receive initialize for a missing profile on both builds, or internal catalog/connection identities for typed startup timeouts on the source build. Each triggering startup sequence reproduced twice per build where noted. No provider tool call, target mutation, data loss or false complete result was observed in these startup failures. Real IDA incidence and downstream client consequences remain unknown.

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The public analysis command attributes selected-runtime startup failures to internal operations instead of the operation requiring that runtime, contrary to the MCP contract and the discriminating RPC-refusal control.
  • Workaround status: unverified
  • Workaround basis: Conforming startup avoids the trigger in the successful fixture, but no practical workaround for a real incompatible or stalled IDA supervisor was verified.

REA version

Current main source commit d5be402fb3dbfc56ac9854c27b5538df20cc2437, package version 6.5.0, built from locked dependencies; separately installed published npm package rea-agents@6.5.0. Both the npm registry and GitHub latest release identified that package as the latest published release.

Runtime and platform

Node.js v24.18.0; Darwin arm64. The source build used repository-pinned npm 11.16.0 and Node. No real IDA engine was used.

Target and artifact context

A synthetic 64-byte ELF x86_64 header passes target admission. A loopback MCP fixture returns the generated input path and active owned-worker identity, one function and its pseudocode. It never executes the target or opens a real IDA database.

Interface

CLI

Earliest failure stage

Setup or provider admission

Relevant output or evidence

  • Expected source: docs/mcp-contracts.md:517–522, selected-runtime failures are reported at the operation needing that runtime; src/application/binary/CompositeProvider.ts:172, lazy operation dispatch; src/cli/coreBinaryCommands.ts:193, canonical decompile operation; src/ida/IdaSessionClient.ts:408, adjacent generic transport projection.
  • Failure source: src/ida/IdaSessionClient.ts:423 and :529; src/ida/IdaMcpConnection.ts:455 and :460; public CLI startup observations below. Contextual health behavior is documented at docs/ida-provider.md:134.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The fixture invokes node scripts/rea.mjs decompile <generated-target.elf> main --provider ida --json. Meaningful fields agreed across both complete repetitions of each build:

Both builds:
pre: exit=1 code=provider_timeout operation="server_health: headless worker still busy in config_json_get" retryable=true timeout_ms=300
post: exit=1 code=provider_timeout operation="server_health: headless worker still busy in config_json_get" retryable=true timeout_ms=300 partial_operation=procedure_pseudo_code failed_operation=procedure_pseudo_code
startup: exit=1 code=execution_failure operation=initialize missing_tools=["list_funcs"]
catalog: exit=1 code=execution_failure operation=procedure_pseudo_code
success: exit=0 Evidence.operation=procedure_pseudo_code

Source build only:
catalog-timeout: exit=1 code=provider_timeout operation=ida_mcp_catalog retryable=true timeout_ms=300
connect-timeout: exit=1 code=provider_timeout operation=ida_mcp_connect retryable=true timeout_ms=300

Published package:
catalog-timeout: exit=1 code=execution_failure operation=procedure_pseudo_code retryable=false
connect-timeout: exit=1 code=execution_failure operation=procedure_pseudo_code retryable=false

Child stderr was empty. For health cases, traces include idb_open, idb_list, health reads, idb_close, and a final empty inventory. Only the post-operation case includes lookup_funcs and decompile. Its partial raw result retains int main(void) { return 42; } and post_operation_identity_check: "started"; normalized target binding remains not_verified. The startup cases dispatch no tools/call.

Source inspection explains public-command reachability. The composite provider opening health check has no instantiated IDA child. The requested analysis then starts that child lazily and reaches startup and the before/after health checks. See src/application/binary/CompositeProvider.ts:172 and src/ida/IdaSessionClient.ts:295.

Typed outcome or recovery expected

For the same missing-profile input on both builds, return a failure with details.operation: "procedure_pseudo_code", retain execution_failure, and preserve missing_tools: ["list_funcs"]. The source build's two startup timeouts should identify that same caller operation while retaining provider_timeout, retryable: true, timeout_ms: 300, captured output and auxiliary-stage diagnostics. All startup failures must still stop before provider tool dispatch. The catalog-refusal and successful controls must retain their correct operation identities and results. Keep the health controls and follow-up partial Evidence behavior intact; changing their documented operation text is outside this report's requested correction.

Analysis or suspected cause

Source-derived reasoning: the IDA session failure handler projects a generic transport adapter failure onto the accepted operation but returns existing typed analysis errors unchanged. Profile validation and typed connection/catalog timeouts enter that handler with internal operation identities. The published package lacks the source build's typed connection/catalog timeout conversion, which explains that observed difference. The missing-profile and typed startup-timeout cases concern attribution of the same selected-runtime admission failure, not whether startup should be rejected.

Additional context

Related #4597 concerns JEB and #4607 concerns Cutter. Their provider implementations are separate. The tested source already includes the JEB attribution fixes, which modify JEB code; the IDA failure handler remains unchanged. No linked fixing commit for the Cutter report was found.

The busy-health wait was introduced by PR #1778, which fixed transient busy responses being rejected as malformed and deliberately carried the busy-tool diagnostic in the internal operation text. That health-specific representation is documented behavior and is contextual evidence here. This report requests correction only for startup failures under the selected-runtime admission contract.

Live searches of open and closed issues and pull requests using IDA operation, attribution, caller, initialization, server_health, IdaSessionClient, ida_mcp_catalog, and ida_mcp_connect found no matching caller-operation report. Existing partial-Evidence-profile and diagnostic-retention reports concern different results. Real IDA occurrence frequency, downstream client consequences and a practical real-engine workaround remain unknown. Only the CLI adapter was exercised.

Originally created by @coygeek on GitHub (Oct 16, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4614 ### 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 Native analysis or decompilation ### Observed behavior The public CLI command `rea decompile <binary> main --provider ida --json` returns a startup failure naming an internal operation instead of the requested `procedure_pseudo_code`. On both tested builds, an IDA headless profile missing `list_funcs` produces `execution_failure` with `details.operation: "initialize"`. On the source build, stalled tool-catalog discovery and initialization also produce typed timeouts identifying `ida_mcp_catalog` and `ida_mcp_connect`. Those two identity failures do not reproduce on the published package, which returns `execution_failure` under the requested operation instead. All startup cases fail before dispatching a provider tool call. The fixture also exercises busy-health waits as context. Both builds retain their documented busy-tool operation text. A follow-up health timeout preserves the completed decompiler reply as partial Evidence under `procedure_pseudo_code`, with target binding explicitly unverified. This report does not classify the documented health-timeout representation as a bug or request a change to it. All observations use a simulated loopback MCP provider. ### Reproduction 1. For the source build, check out the revision listed below, use its pinned Node/npm toolchain, run `npm ci` and `npm run build:cached`, and run this fixture from the checkout root. For the published package, run `npm install --prefix release --ignore-scripts --no-audit --no-fund rea-agents@6.5.0`, change directory to `release/node_modules/rea-agents`, and run the same fixture. Both roots contain `scripts/rea.mjs`. Run it twice per build. Each CLI child receives a fresh HOME and TMPDIR and a whitelist environment. No IDA installation or external service is used. ```sh node --input-type=module <<'NODE' import { createServer } from 'node:http'; import { spawn } from 'node:child_process'; import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import assert from 'node:assert/strict'; const root = await mkdtemp(join(tmpdir(), 'rea-ida-repro-')); const tools = ['idb_open','idb_list','idb_close','server_health','list_funcs','lookup_funcs','find_regex','decompile','disasm','callees','xrefs_to']; try { const target = join(root,'target.elf'); const bytes = Buffer.alloc(64); bytes.set([127,69,76,70,2,1,1]); bytes.writeUInt16LE(62,18); bytes.writeUInt32LE(1,20); bytes.writeUInt16LE(64,52); await writeFile(target,bytes); for (const mode of ['pre','post','startup','catalog-timeout','connect-timeout','catalog','success']) { let database, input, health=0, primary=false; const calls=[]; const server=createServer(async(req,res)=>{ if(req.method!=='POST'){res.writeHead(405).end();return;} let body='';for await(const chunk of req)body+=chunk; const m=JSON.parse(body);if(m.id===undefined){res.writeHead(202).end();return;} const send=result=>res.writeHead(200,{'Content-Type':'application/json'}).end(JSON.stringify({jsonrpc:'2.0',id:m.id,result})); if(m.method==='initialize'){if(mode==='connect-timeout')return;send({protocolVersion:m.params.protocolVersion,capabilities:{tools:{}},serverInfo:{name:'ida-fixture',version:'1'}});return;} if(m.method==='tools/list') {if(mode==='catalog-timeout')return;if(mode==='catalog'){res.writeHead(200,{'Content-Type':'application/json'}).end(JSON.stringify({jsonrpc:'2.0',id:m.id,error:{code:-32603,message:'fixture catalog refused'}}));return;} send({tools:tools.filter(n=>mode!=='startup'||n!=='list_funcs').map(name=>({name,inputSchema:{type:'object',properties:{}}}))});return;} const {name,arguments:a}=m.params;calls.push(name);let p; if(name==='idb_open'){database=a.preferred_session_id;input=a.input_path;p={success:true,session:{session_id:database,input_path:input}};} else if(name==='idb_list')p={sessions:database?[{session_id:database,input_path:input,owned:true,backend:'worker',is_active:true}]:[]}; else if(name==='idb_close'){p={success:true,session_id:database,saved:null,owned:true,backend:'worker'};database=undefined;} else if(name==='server_health'){health++;p=(mode==='pre'&&health>=1)||(mode==='post'&&primary)?{status:'busy',busy_tool:'config_json_get',busy_sec:0}:{status:'ok',input_path:input,idb_path:input+'.i64',module:'target.elf',imagebase:'0x1000',auto_analysis_ready:true};} else if(name==='lookup_funcs')p=a.queries.map(query=>({query,fn:{addr:'0x1000',name:'main',size:'0x20'},error:null})); else if(name==='decompile'){primary=true;p={code:'int main(void) { return 42; }',cursor:{done:true}};} else throw Error(name); send({content:[{type:'text',text:JSON.stringify(p)}]}); }); await new Promise(r=>server.listen(0,'127.0.0.1',r)); try { const home=await mkdtemp(join(root,'home-')); const work=join(home,'work');await mkdir(work); const config=join(home,'ida.json');await writeFile(config,JSON.stringify({url:`http://127.0.0.1:${server.address().port}/mcp`,mode:'headless',timeoutMs:300,workspaceRoot:work})); const child=spawn(process.execPath,['scripts/rea.mjs','decompile',target,'main','--provider','ida','--json'],{env:{PATH:process.env.PATH,HOME:home,TMPDIR:home,REA_IDA_MCP_CONFIG:config,REA_LOG_LEVEL:'silent'}}); let stdout='',stderr='';child.stdout.on('data',x=>stdout+=x);child.stderr.on('data',x=>stderr+=x); const code=await new Promise((resolve,reject)=>{child.on('error',reject);child.on('close',resolve)}); let o;try{o=JSON.parse(stdout);}catch{console.log({mode,code,stdout,stderr});throw Error('not JSON');} assert.equal(stderr,''); assert.equal(database,undefined); const summary={mode,code,calls,error:o.code,operation:o.details?.operation??o.operation,retryable:o.retryable,timeout:o.details?.timeout_ms,missing:o.details?.diagnostics?.missing_tools,partialOperation:o.details?.partial_observation?.operation,failedOperation:o.details?.partial_observation?.normalized_result?.failed_operation}; console.log(JSON.stringify(summary)); if(process.env.REA_REPRO_OUTPUT)await writeFile(join(process.env.REA_REPRO_OUTPUT,mode+'.json'),JSON.stringify(o,null,2)); if(mode==='success'){assert.equal(code,0);assert.equal(o.operation,'procedure_pseudo_code');} else {assert.equal(code,1);assert.equal(summary.operation,mode==='startup'?'initialize':mode==='catalog'?'procedure_pseudo_code':mode==='catalog-timeout'?(o.code==='provider_timeout'?'ida_mcp_catalog':'procedure_pseudo_code'):mode==='connect-timeout'?(o.code==='provider_timeout'?'ida_mcp_connect':'procedure_pseudo_code'):'server_health: headless worker still busy in config_json_get');if(mode==='pre'||mode==='post'){assert.equal(o.code,'provider_timeout');assert.equal(o.retryable,true);assert.equal(summary.timeout,300);}if(mode==='pre'){assert(!calls.includes('lookup_funcs'));assert(!calls.includes('decompile'));assert.equal(o.details?.partial_observation,undefined);}if(mode==='catalog-timeout'||mode==='connect-timeout'){if(o.code==='provider_timeout'){assert.equal(o.retryable,true);assert.equal(summary.timeout,300);}else{assert.equal(o.code,'execution_failure');assert.equal(o.retryable,false);}}if(mode==='startup'){assert.equal(o.code,'execution_failure');assert.deepEqual(summary.missing,['list_funcs']);}if(mode==='post'){assert(calls.includes('decompile'));assert.equal(o.details.partial_observation.normalized_result.target_binding.status,'not_verified');assert.equal(o.details.partial_observation.raw_result.post_operation_identity_check,'started');assert.equal(o.details.partial_observation.raw_result.observations.at(-1).result.code,'int main(void) { return 42; }');assert.equal(summary.partialOperation,'procedure_pseudo_code');assert.equal(summary.failedOperation,'procedure_pseudo_code');}} } finally {server.closeAllConnections();await new Promise(r=>server.close(r));} } } finally {await rm(root,{recursive:true,force:true});} NODE ``` 2. Read `startup`, which omits only `list_funcs` from the advertised profile. It returns `execution_failure` naming `initialize`. The catalog and handshake timeout rows identify internal operations on the source build. The published package instead returns a generic `execution_failure` under the correct requested operation for those two timeout rows. 3. Compare the `catalog` RPC-refusal and `success` controls, which identify the requested operation on both builds. The `pre` and `post` health rows provide contextual observations of the documented busy-tool diagnostic. The first runs no lookup or decompile; the second retains the completed reply as partial Evidence. The fixture confirms that owned workers were closed. 4. All seven rows completed twice per build. The fixture asserts current outputs, including the known build difference for startup timeouts. When checking restoration, change the `startup` expectation and the source build's two startup-timeout identity expectations to the caller's operation. Retain the health, category, diagnostic, partial-result and cleanup controls. ### Expected behavior A startup failure returned for the accepted analysis request should retain its caller operation, `procedure_pseudo_code`, while preserving auxiliary-stage diagnostics, missing tools, retryability, actual timeout budget, captured output and cleanup facts. The MCP contract says provider tools require their selected analysis runtime and that these failures are reported at the operation that needs it. The accepted analysis request triggers lazy IDA startup and needs that runtime. Fail closed on an incompatible profile or stalled initialization. Keep the startup failure classification, actual request budget and missing-tool diagnostics. The ordinary catalog-refusal control already reports the accepted analysis operation. Preserve the independently documented busy-health behavior and its retained partial Evidence. ### Impact and scope The observed defect changes a machine-readable operation identity during IDA startup. Callers inspecting `details.operation` receive `initialize` for a missing profile on both builds, or internal catalog/connection identities for typed startup timeouts on the source build. Each triggering startup sequence reproduced twice per build where noted. No provider tool call, target mutation, data loss or false complete result was observed in these startup failures. Real IDA incidence and downstream client consequences remain unknown. - Impact level: P2 - Assessment status: supported - Impact basis: The public analysis command attributes selected-runtime startup failures to internal operations instead of the operation requiring that runtime, contrary to the MCP contract and the discriminating RPC-refusal control. - Workaround status: unverified - Workaround basis: Conforming startup avoids the trigger in the successful fixture, but no practical workaround for a real incompatible or stalled IDA supervisor was verified. ### REA version Current main source commit `d5be402fb3dbfc56ac9854c27b5538df20cc2437`, package version 6.5.0, built from locked dependencies; separately installed published npm package `rea-agents@6.5.0`. Both the npm registry and GitHub latest release identified that package as the latest published release. ### Runtime and platform Node.js v24.18.0; Darwin arm64. The source build used repository-pinned npm 11.16.0 and Node. No real IDA engine was used. ### Target and artifact context A synthetic 64-byte ELF x86_64 header passes target admission. A loopback MCP fixture returns the generated input path and active owned-worker identity, one function and its pseudocode. It never executes the target or opens a real IDA database. ### Interface CLI ### Earliest failure stage Setup or provider admission ### Relevant output or evidence - Expected source: docs/mcp-contracts.md:517–522, selected-runtime failures are reported at the operation needing that runtime; src/application/binary/CompositeProvider.ts:172, lazy operation dispatch; src/cli/coreBinaryCommands.ts:193, canonical decompile operation; src/ida/IdaSessionClient.ts:408, adjacent generic transport projection. - Failure source: src/ida/IdaSessionClient.ts:423 and :529; src/ida/IdaMcpConnection.ts:455 and :460; public CLI startup observations below. Contextual health behavior is documented at docs/ida-provider.md:134. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The fixture invokes `node scripts/rea.mjs decompile <generated-target.elf> main --provider ida --json`. Meaningful fields agreed across both complete repetitions of each build: ```text Both builds: pre: exit=1 code=provider_timeout operation="server_health: headless worker still busy in config_json_get" retryable=true timeout_ms=300 post: exit=1 code=provider_timeout operation="server_health: headless worker still busy in config_json_get" retryable=true timeout_ms=300 partial_operation=procedure_pseudo_code failed_operation=procedure_pseudo_code startup: exit=1 code=execution_failure operation=initialize missing_tools=["list_funcs"] catalog: exit=1 code=execution_failure operation=procedure_pseudo_code success: exit=0 Evidence.operation=procedure_pseudo_code Source build only: catalog-timeout: exit=1 code=provider_timeout operation=ida_mcp_catalog retryable=true timeout_ms=300 connect-timeout: exit=1 code=provider_timeout operation=ida_mcp_connect retryable=true timeout_ms=300 Published package: catalog-timeout: exit=1 code=execution_failure operation=procedure_pseudo_code retryable=false connect-timeout: exit=1 code=execution_failure operation=procedure_pseudo_code retryable=false ``` Child stderr was empty. For health cases, traces include `idb_open`, `idb_list`, health reads, `idb_close`, and a final empty inventory. Only the post-operation case includes `lookup_funcs` and `decompile`. Its partial raw result retains `int main(void) { return 42; }` and `post_operation_identity_check: "started"`; normalized target binding remains `not_verified`. The startup cases dispatch no tools/call. Source inspection explains public-command reachability. The composite provider opening health check has no instantiated IDA child. The requested analysis then starts that child lazily and reaches startup and the before/after health checks. See src/application/binary/CompositeProvider.ts:172 and src/ida/IdaSessionClient.ts:295. ### Typed outcome or recovery expected For the same missing-profile input on both builds, return a failure with `details.operation: "procedure_pseudo_code"`, retain `execution_failure`, and preserve `missing_tools: ["list_funcs"]`. The source build's two startup timeouts should identify that same caller operation while retaining `provider_timeout`, `retryable: true`, `timeout_ms: 300`, captured output and auxiliary-stage diagnostics. All startup failures must still stop before provider tool dispatch. The catalog-refusal and successful controls must retain their correct operation identities and results. Keep the health controls and follow-up partial Evidence behavior intact; changing their documented operation text is outside this report's requested correction. ### Analysis or suspected cause Source-derived reasoning: the IDA session failure handler projects a generic transport adapter failure onto the accepted operation but returns existing typed analysis errors unchanged. Profile validation and typed connection/catalog timeouts enter that handler with internal operation identities. The published package lacks the source build's typed connection/catalog timeout conversion, which explains that observed difference. The missing-profile and typed startup-timeout cases concern attribution of the same selected-runtime admission failure, not whether startup should be rejected. ### Additional context Related [#4597](https://github.com/morluto/rea/issues/4597) concerns JEB and [#4607](https://github.com/morluto/rea/issues/4607) concerns Cutter. Their provider implementations are separate. The tested source already includes the JEB attribution fixes, which modify JEB code; the IDA failure handler remains unchanged. No linked fixing commit for the Cutter report was found. The busy-health wait was introduced by [PR #1778](https://github.com/morluto/rea/pull/1778), which fixed transient busy responses being rejected as malformed and deliberately carried the busy-tool diagnostic in the internal operation text. That health-specific representation is documented behavior and is contextual evidence here. This report requests correction only for startup failures under the selected-runtime admission contract. Live searches of open and closed issues and pull requests using IDA operation, attribution, caller, initialization, `server_health`, `IdaSessionClient`, `ida_mcp_catalog`, and `ida_mcp_connect` found no matching caller-operation report. Existing partial-Evidence-profile and diagnostic-retention reports concern different results. Real IDA occurrence frequency, downstream client consequences and a practical real-engine workaround remain unknown. Only the CLI adapter was exercised.
Author
Owner

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

Implemented in 8f65d7f256, pushed to main.

Startup stays cached, while each accepted request receives its own typed failure with the caller operation. details.diagnostics.startup_operation retains the original initialization/catalog/connection/database-open stage; existing profile diagnostics, timeout budgets, resource limits, output and recovery remain available. The original cached cause is unchanged. The redundant typed-error branch was removed. Documented busy-health operation text and post-operation partial Evidence remain covered.

The same boundary audit found #4633: cancellation copied historical cleanup without a live completion dependency. Cancellation now binds to the exact source owner, so denied release remains incomplete and confirmed release clears the cancellation outcome, including release before wrapping.

Executed validation:

  • 130 tests across nine focused files passed, including five public CLI startup/refusal/success cases, shared startup attribution for distinct callers, resource limits, failed cleanup retry, release before/after cancellation, retained one-shot cleanup, stderr diagnostics, busy health, and partial operation Evidence.
  • Four source controls reran after correcting the fixture's explicit HTTP headers type.
  • Typecheck/lint, docs checks, five documentation conformance controls, normal commit hooks and normal pre-push checks passed. Read-only source review found no blocker.
  • Independent compiled main reproduction for #4633 returned cleanup_incomplete after release in both cases. The repaired build returns cancelled after release and immediately when release preceded wrapping.

Bounded cost comparison: same completed public CLI missing-profile refusal through a loopback SDK supervisor; two alternating warm repeats per build plus separately recorded first processes. Baseline/candidate medians: elapsed 0.48933/0.47599 seconds, CPU 0.70581/0.70182 seconds, sampled peak process-family RSS 423,223,296/441,040,896 bytes, raw reply tokens 247/254 (cl100k_base). A full duplicate startup projection was rejected after measurement; the compact stage adds seven estimated tokens. No speed or memory improvement claimed. True cold, whole-agent, MCP/catalog/request/retry and native-engine costs remain unmeasured.

Scope: public CLI tests use a controlled loopback supervisor; cancellation/ownership controls use a controlled connection and actual cleanup lease. No native IDA engine or attached GUI was used, and no real-engine support claim is added. Heavy Qodo/profiler jobs were not restarted.

<!-- gh-comment-id:6227813965 --> @morluto commented on GitHub (Oct 17, 2026): Implemented in 8f65d7f2569c36707da26ad04b64488fcaa7664d, pushed to main. Startup stays cached, while each accepted request receives its own typed failure with the caller operation. `details.diagnostics.startup_operation` retains the original initialization/catalog/connection/database-open stage; existing profile diagnostics, timeout budgets, resource limits, output and recovery remain available. The original cached cause is unchanged. The redundant typed-error branch was removed. Documented busy-health operation text and post-operation partial Evidence remain covered. The same boundary audit found #4633: cancellation copied historical cleanup without a live completion dependency. Cancellation now binds to the exact source owner, so denied release remains incomplete and confirmed release clears the cancellation outcome, including release before wrapping. Executed validation: - 130 tests across nine focused files passed, including five public CLI startup/refusal/success cases, shared startup attribution for distinct callers, resource limits, failed cleanup retry, release before/after cancellation, retained one-shot cleanup, stderr diagnostics, busy health, and partial operation Evidence. - Four source controls reran after correcting the fixture's explicit HTTP headers type. - Typecheck/lint, docs checks, five documentation conformance controls, normal commit hooks and normal pre-push checks passed. Read-only source review found no blocker. - Independent compiled main reproduction for #4633 returned cleanup_incomplete after release in both cases. The repaired build returns cancelled after release and immediately when release preceded wrapping. Bounded cost comparison: same completed public CLI missing-profile refusal through a loopback SDK supervisor; two alternating warm repeats per build plus separately recorded first processes. Baseline/candidate medians: elapsed 0.48933/0.47599 seconds, CPU 0.70581/0.70182 seconds, sampled peak process-family RSS 423,223,296/441,040,896 bytes, raw reply tokens 247/254 (cl100k_base). A full duplicate startup projection was rejected after measurement; the compact stage adds seven estimated tokens. No speed or memory improvement claimed. True cold, whole-agent, MCP/catalog/request/retry and native-engine costs remain unmeasured. Scope: public CLI tests use a controlled loopback supervisor; cancellation/ownership controls use a controlled connection and actual cleanup lease. No native IDA engine or attached GUI was used, and no real-engine support claim is added. Heavy Qodo/profiler jobs were not restarted.
Author
Owner

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

Closing as addressed on main by 8f65d7f25. IDA startup failures now identify the accepted analysis operation while retaining startup-stage diagnostics, missing-tool facts, actual timeout budgets and cleanup ownership. I verified the committed source and regression coverage; I have not rerun the runtime reproduction.

<!-- gh-comment-id:6244117916 --> @coygeek commented on GitHub (Oct 18, 2026): Closing as addressed on `main` by [8f65d7f25](https://github.com/morluto/rea/commit/8f65d7f2569c36707da26ad04b64488fcaa7664d). IDA startup failures now identify the accepted analysis operation while retaining startup-stage diagnostics, missing-tool facts, actual timeout budgets and cleanup ownership. I verified the committed source and regression coverage; I have not rerun the runtime reproduction.
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#2399
No description provided.