[GH-ISSUE #2549] fix(ida): CLI remains alive after reporting retained cleanup #865

Closed
opened 2026-10-15 18:29:07 +00:00 by bsakaguchi · 1 comment
Owner

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

Summary

A one-shot headless IDA command prints its final failure result but remains running when its stdio supervisor never completes database open. REA correctly reports cleanup_incomplete and retains the private workspace, but the retained supervisor child and its three pipes keep the command alive. Shell scripts and agents waiting for the command cannot advance. This was reproduced with a disposable protocol fixture through both the current source CLI and a standalone executable dispatcher, without running IDA or the target.

Steps to reproduce

Use the baseline below with its compiled CLI available as rea. In a disposable directory outside the checkout, provide a valid supported native executable named target. The observed test copied a small system Mach-O executable; a large binary is unnecessary. The fixture implements the supervisor protocol and deliberately never answers idb_open, while answering cleanup inventory with no sessions.

  1. Save this fixture as supervisor.mjs in that directory.
import { createInterface } from 'node:readline';
const names = ['idb_open', 'idb_list', 'idb_close', 'server_health',
  'list_funcs', 'lookup_funcs', 'find_regex', 'decompile', 'disasm',
  'callees', 'xrefs_to'];
createInterface({ input: process.stdin }).on('line', line => {
  const request = JSON.parse(line);
  if (request.id === undefined) return;
  let result;
  if (request.method === 'initialize') {
    result = {
      protocolVersion: request.params.protocolVersion,
      capabilities: { tools: {} },
      serverInfo: { name: 'fixture', version: '1' }
    };
  } else if (request.method === 'tools/list') {
    result = { tools: names.map(name => ({
      name, inputSchema: { type: 'object', properties: {} }
    })) };
  } else if (request.method === 'tools/call' &&
             request.params.name === 'idb_list') {
    result = { content: [], structuredContent: { sessions: [] } };
  } else {
    return;
  }
  process.stdout.write(JSON.stringify({
    jsonrpc: '2.0', id: request.id, result
  }) + '\n');
});
  1. Generate a headless stdio registration with a 500 ms request deadline and a private workspace parent in the disposable directory.
node --input-type=module -e '
import { writeFileSync } from "node:fs";
import { resolve } from "node:path";
writeFileSync("registration.json", JSON.stringify({
  command: process.execPath,
  args: [resolve("supervisor.mjs")],
  mode: "headless",
  timeoutMs: 500,
  workspaceRoot: process.cwd()
}));'
REA_ANALYSIS_PROVIDER=ida REA_IDA_MCP_CONFIG="$PWD/registration.json" \
  rea decompile target main --json
  1. After the command prints cleanup_incomplete, observe that it still has not returned a terminal status. The local standalone probe remained alive for a further 2,000 ms. Its private workspace remained. The source CLI invocation independently returned the same output while retaining referenced child and pipe handles.

  2. For this fixture only, stop its supervisor child. The local standalone probe then exited with status 1 within the three-second control window. The fixture creates no actual worker or database. Stopping a real supervisor does not establish worker release, so that control is not a production cleanup recommendation.

Expected behavior

A one-shot CLI command that cannot complete must return its structured failure and exit status 1. When database release remains unverified, it must preserve the incomplete-cleanup diagnosis and retained private workspace. Preserving those facts should still let the finished command return a terminal status.

Actual behavior

The source CLI finished its command action and returned cleanup_incomplete. Its supervisor child had a referenced process handle, and stdin, stdout and stderr each had a referenced pipe handle. The standalone CLI also printed its final failure result, then remained running throughout the two-second observation. Its exit status was unavailable until the fixture supervisor was stopped, after which it exited 1. The result's cleanup reason said the database open had not returned a completed response and inventory could not prove that the request stopped. No real worker, IDA database or engine behavior was observed.

Evidence

  • Expected source: docs/cli.md:254-266 defines failure exit status 1; src/cli.ts:62-65 describes the CLI as one-shot; docs/ida-provider.md:87 requires retained workspaces and incomplete-cleanup reporting when release cannot be verified.
  • Failure source: The executed source CLI and standalone dispatcher probe observed final cleanup_incomplete output and retained live references. src/ida/IdaSessionClient.ts:444-452 leaves open incomplete after the request timeout, and :641-659 returns cleanup failure before closing the connection. src/ida/IdaMcpConnection.ts:142-160 creates the stdio child with three pipes. src/application/DirectAnalysisCleanup.ts:30-31 returns the final result after the cleanup failure.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

One focused verification test passed. The source CLI's real SDK child had childRef: true and pipeRefs: [true, true, true] after the command returned. The standalone dispatcher printed final JSON, remained alive for 2,000 ms, and exited 1 after the fixture supervisor stopped. The two invocations each retained one private workspace. Source tracing establishes why those references continue holding the event loop while the supervisor remains alive; the local runtime observation was bounded and does not claim an indefinitely measured hang.

The timeout occurred during database open. A tool timeout after a completed open does not necessarily reproduce this problem because cleanup can succeed. The fixture supplies all required modern tools and returns a completed empty inventory, so missing catalog entries and a stalled cleanup inventory do not explain the failure.

Related work: another author's open PR #1841 addresses this exact CLI exit failure. It was unmerged when routing was checked. Its body contains separately attributed real-provider observations; this report's local evidence is the protocol-fixture reproduction described above.

Restoration check

Repeat the fixture with the same registration and native input. Require the one-shot CLI to print cleanup_incomplete, retain the private workspace, and exit with status 1 within a bounded interval after printing the result. Preserve accurate uncertainty about worker release. Add a successful-close control that returns the normal status and removes its workspace. In a live MCP session, verify that incomplete cleanup still retains the owner and permits a later close retry. Merely deleting the workspace or claiming successful release would fail the retained-resource contract.

Runtime or environment

The failure was verified at source checkout a2d8b94dd50dab8d966239dd63ab84718aa8b4b4 and is still present at 8b181716aca2cb467be499cba226abfb5ae19141, with package metadata rea-agents 6.4.0, Node.js v26.10.0 and npm 11.19.1 on macOS. The source CLI ran through the installed locked dependencies. A separate standalone observation used the checkout's already-present compiled dispatcher. The latest published GitHub release was 6.4.0. No licensed IDA engine or real upstream worker was run.

Triage assessment

  • Impact level: P1
  • Assessment status: supported
  • Impact basis: A supported headless stdio registration with an uncompleted open blocks the primary CLI workflow from returning a terminal status after its final result, preventing shell and agent sequencing. Both the source path's retained references and the standalone nontermination were directly observed. The frequency with real providers was not measured locally.
  • Workaround status: unverified
  • Workaround basis: Stopping the synthetic supervisor allowed the fixture CLI to exit, but stopping a real supervisor does not prove worker release or settle retained resources. Increasing the deadline may help a merely slow open, but it does not settle an unresponsive supervisor. No safe practical production recovery was verified.
Originally created by @coygeek on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2549 ## Summary A one-shot headless IDA command prints its final failure result but remains running when its stdio supervisor never completes database open. REA correctly reports `cleanup_incomplete` and retains the private workspace, but the retained supervisor child and its three pipes keep the command alive. Shell scripts and agents waiting for the command cannot advance. This was reproduced with a disposable protocol fixture through both the current source CLI and a standalone executable dispatcher, without running IDA or the target. ## Steps to reproduce Use the baseline below with its compiled CLI available as `rea`. In a disposable directory outside the checkout, provide a valid supported native executable named `target`. The observed test copied a small system Mach-O executable; a large binary is unnecessary. The fixture implements the supervisor protocol and deliberately never answers `idb_open`, while answering cleanup inventory with no sessions. 1. Save this fixture as `supervisor.mjs` in that directory. ```javascript import { createInterface } from 'node:readline'; const names = ['idb_open', 'idb_list', 'idb_close', 'server_health', 'list_funcs', 'lookup_funcs', 'find_regex', 'decompile', 'disasm', 'callees', 'xrefs_to']; createInterface({ input: process.stdin }).on('line', line => { const request = JSON.parse(line); if (request.id === undefined) return; let result; if (request.method === 'initialize') { result = { protocolVersion: request.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: 'fixture', version: '1' } }; } else if (request.method === 'tools/list') { result = { tools: names.map(name => ({ name, inputSchema: { type: 'object', properties: {} } })) }; } else if (request.method === 'tools/call' && request.params.name === 'idb_list') { result = { content: [], structuredContent: { sessions: [] } }; } else { return; } process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: request.id, result }) + '\n'); }); ``` 2. Generate a headless stdio registration with a 500 ms request deadline and a private workspace parent in the disposable directory. ```sh node --input-type=module -e ' import { writeFileSync } from "node:fs"; import { resolve } from "node:path"; writeFileSync("registration.json", JSON.stringify({ command: process.execPath, args: [resolve("supervisor.mjs")], mode: "headless", timeoutMs: 500, workspaceRoot: process.cwd() }));' REA_ANALYSIS_PROVIDER=ida REA_IDA_MCP_CONFIG="$PWD/registration.json" \ rea decompile target main --json ``` 3. After the command prints `cleanup_incomplete`, observe that it still has not returned a terminal status. The local standalone probe remained alive for a further 2,000 ms. Its private workspace remained. The source CLI invocation independently returned the same output while retaining referenced child and pipe handles. 4. For this fixture only, stop its supervisor child. The local standalone probe then exited with status 1 within the three-second control window. The fixture creates no actual worker or database. Stopping a real supervisor does not establish worker release, so that control is not a production cleanup recommendation. ## Expected behavior A one-shot CLI command that cannot complete must return its structured failure and exit status 1. When database release remains unverified, it must preserve the incomplete-cleanup diagnosis and retained private workspace. Preserving those facts should still let the finished command return a terminal status. ## Actual behavior The source CLI finished its command action and returned `cleanup_incomplete`. Its supervisor child had a referenced process handle, and stdin, stdout and stderr each had a referenced pipe handle. The standalone CLI also printed its final failure result, then remained running throughout the two-second observation. Its exit status was unavailable until the fixture supervisor was stopped, after which it exited 1. The result's cleanup reason said the database open had not returned a completed response and inventory could not prove that the request stopped. No real worker, IDA database or engine behavior was observed. ## Evidence - Expected source: `docs/cli.md:254-266` defines failure exit status 1; `src/cli.ts:62-65` describes the CLI as one-shot; `docs/ida-provider.md:87` requires retained workspaces and incomplete-cleanup reporting when release cannot be verified. - Failure source: The executed source CLI and standalone dispatcher probe observed final `cleanup_incomplete` output and retained live references. `src/ida/IdaSessionClient.ts:444-452` leaves open incomplete after the request timeout, and `:641-659` returns cleanup failure before closing the connection. `src/ida/IdaMcpConnection.ts:142-160` creates the stdio child with three pipes. `src/application/DirectAnalysisCleanup.ts:30-31` returns the final result after the cleanup failure. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete One focused verification test passed. The source CLI's real SDK child had `childRef: true` and `pipeRefs: [true, true, true]` after the command returned. The standalone dispatcher printed final JSON, remained alive for 2,000 ms, and exited 1 after the fixture supervisor stopped. The two invocations each retained one private workspace. Source tracing establishes why those references continue holding the event loop while the supervisor remains alive; the local runtime observation was bounded and does not claim an indefinitely measured hang. The timeout occurred during database open. A tool timeout after a completed open does not necessarily reproduce this problem because cleanup can succeed. The fixture supplies all required modern tools and returns a completed empty inventory, so missing catalog entries and a stalled cleanup inventory do not explain the failure. Related work: another author's open [PR #1841](https://github.com/morluto/rea/pull/1841) addresses this exact CLI exit failure. It was unmerged when routing was checked. Its body contains separately attributed real-provider observations; this report's local evidence is the protocol-fixture reproduction described above. ## Restoration check Repeat the fixture with the same registration and native input. Require the one-shot CLI to print `cleanup_incomplete`, retain the private workspace, and exit with status 1 within a bounded interval after printing the result. Preserve accurate uncertainty about worker release. Add a successful-close control that returns the normal status and removes its workspace. In a live MCP session, verify that incomplete cleanup still retains the owner and permits a later close retry. Merely deleting the workspace or claiming successful release would fail the retained-resource contract. ## Runtime or environment The failure was verified at source checkout `a2d8b94dd50dab8d966239dd63ab84718aa8b4b4` and is still present at `8b181716aca2cb467be499cba226abfb5ae19141`, with package metadata `rea-agents` 6.4.0, Node.js v26.10.0 and npm 11.19.1 on macOS. The source CLI ran through the installed locked dependencies. A separate standalone observation used the checkout's already-present compiled dispatcher. The latest published GitHub release was 6.4.0. No licensed IDA engine or real upstream worker was run. ## Triage assessment - Impact level: P1 - Assessment status: supported - Impact basis: A supported headless stdio registration with an uncompleted open blocks the primary CLI workflow from returning a terminal status after its final result, preventing shell and agent sequencing. Both the source path's retained references and the standalone nontermination were directly observed. The frequency with real providers was not measured locally. - Workaround status: unverified - Workaround basis: Stopping the synthetic supervisor allowed the fixture CLI to exit, but stopping a real supervisor does not prove worker release or settle retained resources. Increasing the deadline may help a merely slow open, but it does not settle an unresponsive supervisor. No safe practical production recovery was verified.
bsakaguchi 2026-10-15 18:29:07 +00:00
Author
Owner

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

Resolved on main by 7369b5ee6.

The one-shot CLI exits after reporting retained cleanup without waiting indefinitely on the supervisor; cleanup ownership remains retained. This uses the production CLI with a controlled supervisor boundary, not a real IDA engine.

Rechecked on main b8510ee00 in the current focused run: 20 test files passed, 225 tests passed and one platform-dependent test skipped. Relevant regression coverage: tests/boundary/cli/idaRetainedCleanupCli.test.ts.

<!-- gh-comment-id:6174990384 --> @morluto commented on GitHub (Oct 14, 2026): Resolved on main by 7369b5ee6. The one-shot CLI exits after reporting retained cleanup without waiting indefinitely on the supervisor; cleanup ownership remains retained. This uses the production CLI with a controlled supervisor boundary, not a real IDA engine. Rechecked on main b8510ee00 in the current focused run: 20 test files passed, 225 tests passed and one platform-dependent test skipped. Relevant regression coverage: tests/boundary/cli/idaRetainedCleanupCli.test.ts.
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#865
No description provided.