[GH-ISSUE #1710] [Bug] Headless IDA cleanup cannot recover after the supervisor reaps an owned worker #572

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

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

Before submitting

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

Area

Native analysis or decompilation

Observed behavior

After a successful headless open, an analysis request can cause the upstream supervisor to reap and unregister an expired or exited worker. REA then repeatedly requires a successful close response for that now-absent database. It never reaches inventory verification, retains the private input workspace and its connection, and reports incomplete cleanup on every retry. This also blocks later target opens in that REA MCP process through its pending-cleanup state.

Reproduction

The executed reproduction uses the production REA session implementation at its connection boundary. From a built checkout, save the following script as reproduce-cleanup.mjs outside the repository and run node reproduce-cleanup.mjs /absolute/path/to/rea. The script copies a local system binary only for hashing, does not execute it, and removes its disposable input and workspaces after collecting the result.

import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';

const repo = resolve(process.argv[2] || '.');
const { IdaSessionClient } = await import(pathToFileURL(join(repo, 'dist/ida/IdaSessionClient.js')));
const { decodeIdaToolResult } = await import(pathToFileURL(join(repo, 'dist/ida/IdaMcpConnection.js')));
const tools = ['idb_open', 'idb_list', 'idb_close', 'server_health', 'list_funcs', 'lookup_funcs', 'find_regex', 'decompile', 'disasm', 'callees', 'xrefs_to'];

async function runCase(mode) {
  const root = await mkdtemp('/tmp/agent-work.');
  const path = join(root, 'target.elf');
  const bytes = await readFile('/bin/echo'); // copied for digest only; never executed
  await writeFile(path, bytes);
  const target = {path, sha256: createHash('sha256').update(bytes).digest('hex'), kind:'executable', format:'mach-o', architecture:'arm64', availableArchitectures:['arm64']};
  let database, inputPath, connectionCloses = 0;
  let expired = false;
  const calls = [];
  const connection = {
    async connect() { return tools; },
    serverInfo() { return {name:'lifecycle-boundary-fixture', version:'1'}; },
    async close() { connectionCloses++; },
    async call(name, args) {
      calls.push(name);
      if (name === 'idb_open') {
        if (mode === 'open-error-control') return {error:'fixture rejects open after completed request'};
        database = args.preferred_session_id; inputPath = args.input_path;
        return {success:true, session:{session_id:database, input_path:inputPath}};
      }
      if (name === 'idb_list') return {sessions: database ? [{session_id:database,input_path:inputPath,owned:true,backend:'worker',is_active:true}] : []};
      if (name === 'server_health') {
        if (expired) { database = undefined; return decodeIdaToolResult({isError:true,content:[{type:'text',text:JSON.stringify({error:'Worker for session is not reachable'})}]}); }
        return {status:'ok',input_path:inputPath,idb_path:inputPath+'.i64',imagebase:'0x1000',auto_analysis_ready:true,module:'target.elf'};
      }
      if (name === 'idb_close') {
        if (database === undefined) return {error:'Session not found: '+args.database};
        const session_id = database; database = undefined;
        return {success:true,session_id,owned:true,backend:'worker',saved:null};
      }
      throw new Error('Unexpected call: '+name);
    },
  };
  const client = new IdaSessionClient({command:'fixture',args:[],env:{},mode:'headless',timeoutMs:1000,workspaceRoot:root},target,connection);
  try {
    const opened = await client.execute('health', {});
    assert.equal(opened.ok, mode !== 'open-error-control');
    let failedRead;
    if (mode === 'expired-worker') { expired = true; failedRead = await client.execute('health', {}); assert.equal(failedRead.ok, false); }
    const closeStart = calls.length;
    const closes = [await client.close(), await client.close()];
    const workspacePresent = (await readdir(root)).some(x => x.startsWith('rea-ida-'));
    const inventory = await connection.call('idb_list', {});
    if (mode === 'expired-worker') {
      assert.deepEqual(closes.map(x=>x.ok), [false,false]);
      assert.equal(workspacePresent,true); assert.equal(connectionCloses,0);
      assert.deepEqual(inventory.sessions,[]);
      assert.deepEqual(calls.slice(closeStart,-1), ['idb_close','idb_close']);
    } else {
      assert.deepEqual(closes.map(x=>x.ok), [true,true]); assert.equal(workspacePresent,false); assert.equal(connectionCloses,1);
    }
    return {mode,openOk:opened.ok,failedReadOk:failedRead?.ok,closeResults:closes.map(x=>({ok:x.ok,...(!x.ok?{cleanupIncomplete:x.error.cleanupIncomplete,reason:x.error.cause?.message}: {})})),workspacePresent,connectionCloses,inventory,calls};
  } finally { await rm(root,{recursive:true,force:true}); }
}
for (const mode of ['live-worker-control','expired-worker','open-error-control']) console.log(JSON.stringify(await runCase(mode)));

To verify the fixture's producer response sequence independently, save the following as reproduce-supervisor.py and run python3 reproduce-supervisor.py /absolute/path/to/ida-pro-mcp against the stated upstream checkout. This invokes its actual supervisor code with an already-exited synthetic process, disables instance discovery, and does not start a subprocess, connect to a socket or import IDA.

"""Exercise upstream supervisor reaping with a synthetic already-exited process.

No IDA modules, subprocesses, sockets or ambient discovery are used.
"""
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(sys.argv[1]).resolve() / "src"))
from ida_pro_mcp import idalib_supervisor as mod

class ExitedProcess:
    pid = 424242
    def poll(self):
        return 0

mod._discovery.discover_instances = lambda: []
sup = mod.IdalibSupervisor(mod.McpServer("disposable-lifecycle-probe"), max_workers=1)
mod.supervisor = sup
session = mod.WorkerSession(session_id="fixture-session", input_path="/fixture/target", filename="target", process=ExitedProcess())
sup.sessions[session.session_id] = session
sup.path_to_session[sup._path_key(session.input_path)] = session.session_id
assert session.session_id in sup.sessions
health = mod._handle_tools_call({"jsonrpc":"2.0", "id":1, "method":"tools/call", "params":{"name":"server_health", "arguments":{"database":session.session_id}}})
assert health["result"]["isError"] is True
assert session.session_id not in sup.sessions
closed = mod.idb_close(session.session_id, save=False)
assert closed == {"error":"Session not found: fixture-session"}
inventory = mod.idb_list()
assert inventory == {"sessions":[], "count":0}
assert "idapro" not in sys.modules and "idaapi" not in sys.modules
print(json.dumps({"health":health,"close":closed,"inventory":inventory,"ida_modules_imported":False}))

The corresponding real-engine sequence was not executed: configure the documented headless supervisor, call open_binary with provider_id: "ida", let its worker exit or expire, make an analysis request whose preliminary server_health call reaps and unregisters that worker, then call close_binary twice. The intervening request matters; idle expiration alone does not prove registry removal. Upstream's idb_open default minimum idle TTL is 600 seconds, with load-time adjustment in its worker lifecycle.

Expected behavior

After a completed open and a supervisor-confirmed worker release, close should verify the session and private input are absent from discovery, release the connection, remove the private workspace and succeed on retry. It must continue retaining the workspace when release cannot be verified or another worker still references that input. Live-worker close must preserve save: false.

Impact and scope

A previously opened headless session cannot complete cleanup after an analysis health request causes the supervisor to unregister its exited worker. Repeated close retains the private workspace and connection. Later target opens in that REA process encounter the retained pending-cleanup owner. That target-transition consequence is source-traced, rather than observed through a complete MCP/IDA workflow.

REA version

The production REA adapter was compiled from current main revision 693ed332b58d05192f55b8192d2c0c4bc4f4824a and separately tested from the extracted latest published npm artifact, rea-agents 6.4.0. All ten compiled dist/ida/ JavaScript files matched the published artifact byte for byte. The checks ran on macOS 27.0 arm64 with Node.js v26.10.0. The upstream supervisor checkout was revision c133c3853faa111a9b00ee615c013b720d0c4acd, whose project metadata names version 2.0.0. Python 3.12.13 ran the actual supervisor synthetic-process check. No licensed IDA engine, live worker, GUI database or target executable was run.

Runtime and platform

The measured runtime and platform are stated in the version field above.

Target and artifact context

The adapter fixture copies the local system Mach-O executable only to exercise input hashing and private workspace ownership. It does not execute or analyze that binary. The supervisor fixture uses an already-exited synthetic process and imports no IDA modules.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

One fresh adapter probe against each tested build returns cleanupIncomplete: true for both close attempts in the reaped-worker case. The private workspace remains, connection close count stays zero, and cleanup calls only idb_close twice. A separate inventory call reports no sessions. The live-worker control and ordinary completed open-rejection control both close successfully and remove their workspaces. Both builds produced the same failure and passing control results.

The actual upstream synthetic-process check returns this sequence:

{"health_error":"Worker for session 'fixture-session' is not reachable","close":{"error":"Session not found: fixture-session"},"inventory":{"sessions":[],"count":0}}

REA parses the absent-session close error against its successful-close-only schema and returns validation errors for missing success/session/ownership fields. It does not reach idb_list, connection close or workspace removal. The additional claim that future target opens are blocked follows from the retained pending-cleanup owner in BinarySession; that public-session consequence was source-traced rather than run end to end.

  • Expected source: docs/ida-provider.md, headless lifecycle and cleanup paragraphs, promises verified release followed by private workspace removal; AGENTS.md requires retaining resources when cleanup remains uncertain.
  • Failure source: The inline production-adapter probe observes repeated failed close with empty inventory and a retained workspace; the inline upstream check confirms the exact error responses. src/ida/IdaSessionClient.ts:412-479 reaches inventory only after a successful stale-ID close, and src/application/binary/BinarySession.ts:246-247,408-434 retains and retries the failed owner.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Evidence provenance details: observed production-adapter boundary failure and actual upstream supervisor behavior with a synthetic exited process; the real idle-expiration prerequisite and public pending-open consequence are source-derived.

Local verification details: reproduced once against each tested build with disposable fixtures, plus one actual upstream supervisor synthetic-process check. No real IDA engine or complete CLI/MCP engine workflow was run.

Reproduction completeness details: complete for the adapter failure and supervisor response sequence reproduced above.

Pinned source breadcrumbs: REA close path, pending cleanup, and upstream reap/close behavior.

Typed outcome or recovery expected

Repeat the reaped-worker probe and require both close calls to succeed, the private workspace to be absent, and the connection to close once after verified empty discovery. Exercise the public session transition and require a later target open to proceed after that cleanup. Preserve the passing live-worker and completed-open-rejection controls. Add a discriminating case where inventory is unavailable or still contains a worker using the private input; that case must continue reporting incomplete cleanup and retaining the workspace.

Triage assessment

  • Impact level: P1
  • Assessment status: provisional
  • Impact basis: Cleanup failure leaves a pending owner that source inspection shows blocking later target opens in the same REA process, preventing the core target-transition workflow with no verified practical recovery. The cleanup failure was directly observed at production boundaries with synthetic fixtures, while real-engine incidence and practical recovery remain unmeasured. The live-worker and completed-open-rejection controls pass.
  • Workaround status: unverified
  • Workaround basis: Restarting REA should clear its process-local pending owner, followed by manual cleanup only after independently verifying upstream inventory absence. That complete recovery was not exercised against a real IDA engine, so its practical availability and safety are not established here.
Originally created by @coygeek on GitHub (Oct 11, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1710 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior After a successful headless open, an analysis request can cause the upstream supervisor to reap and unregister an expired or exited worker. REA then repeatedly requires a successful close response for that now-absent database. It never reaches inventory verification, retains the private input workspace and its connection, and reports incomplete cleanup on every retry. This also blocks later target opens in that REA MCP process through its pending-cleanup state. ### Reproduction The executed reproduction uses the production REA session implementation at its connection boundary. From a built checkout, save the following script as `reproduce-cleanup.mjs` outside the repository and run `node reproduce-cleanup.mjs /absolute/path/to/rea`. The script copies a local system binary only for hashing, does not execute it, and removes its disposable input and workspaces after collecting the result. ```javascript import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const repo = resolve(process.argv[2] || '.'); const { IdaSessionClient } = await import(pathToFileURL(join(repo, 'dist/ida/IdaSessionClient.js'))); const { decodeIdaToolResult } = await import(pathToFileURL(join(repo, 'dist/ida/IdaMcpConnection.js'))); const tools = ['idb_open', 'idb_list', 'idb_close', 'server_health', 'list_funcs', 'lookup_funcs', 'find_regex', 'decompile', 'disasm', 'callees', 'xrefs_to']; async function runCase(mode) { const root = await mkdtemp('/tmp/agent-work.'); const path = join(root, 'target.elf'); const bytes = await readFile('/bin/echo'); // copied for digest only; never executed await writeFile(path, bytes); const target = {path, sha256: createHash('sha256').update(bytes).digest('hex'), kind:'executable', format:'mach-o', architecture:'arm64', availableArchitectures:['arm64']}; let database, inputPath, connectionCloses = 0; let expired = false; const calls = []; const connection = { async connect() { return tools; }, serverInfo() { return {name:'lifecycle-boundary-fixture', version:'1'}; }, async close() { connectionCloses++; }, async call(name, args) { calls.push(name); if (name === 'idb_open') { if (mode === 'open-error-control') return {error:'fixture rejects open after completed request'}; database = args.preferred_session_id; inputPath = args.input_path; return {success:true, session:{session_id:database, input_path:inputPath}}; } if (name === 'idb_list') return {sessions: database ? [{session_id:database,input_path:inputPath,owned:true,backend:'worker',is_active:true}] : []}; if (name === 'server_health') { if (expired) { database = undefined; return decodeIdaToolResult({isError:true,content:[{type:'text',text:JSON.stringify({error:'Worker for session is not reachable'})}]}); } return {status:'ok',input_path:inputPath,idb_path:inputPath+'.i64',imagebase:'0x1000',auto_analysis_ready:true,module:'target.elf'}; } if (name === 'idb_close') { if (database === undefined) return {error:'Session not found: '+args.database}; const session_id = database; database = undefined; return {success:true,session_id,owned:true,backend:'worker',saved:null}; } throw new Error('Unexpected call: '+name); }, }; const client = new IdaSessionClient({command:'fixture',args:[],env:{},mode:'headless',timeoutMs:1000,workspaceRoot:root},target,connection); try { const opened = await client.execute('health', {}); assert.equal(opened.ok, mode !== 'open-error-control'); let failedRead; if (mode === 'expired-worker') { expired = true; failedRead = await client.execute('health', {}); assert.equal(failedRead.ok, false); } const closeStart = calls.length; const closes = [await client.close(), await client.close()]; const workspacePresent = (await readdir(root)).some(x => x.startsWith('rea-ida-')); const inventory = await connection.call('idb_list', {}); if (mode === 'expired-worker') { assert.deepEqual(closes.map(x=>x.ok), [false,false]); assert.equal(workspacePresent,true); assert.equal(connectionCloses,0); assert.deepEqual(inventory.sessions,[]); assert.deepEqual(calls.slice(closeStart,-1), ['idb_close','idb_close']); } else { assert.deepEqual(closes.map(x=>x.ok), [true,true]); assert.equal(workspacePresent,false); assert.equal(connectionCloses,1); } return {mode,openOk:opened.ok,failedReadOk:failedRead?.ok,closeResults:closes.map(x=>({ok:x.ok,...(!x.ok?{cleanupIncomplete:x.error.cleanupIncomplete,reason:x.error.cause?.message}: {})})),workspacePresent,connectionCloses,inventory,calls}; } finally { await rm(root,{recursive:true,force:true}); } } for (const mode of ['live-worker-control','expired-worker','open-error-control']) console.log(JSON.stringify(await runCase(mode))); ``` To verify the fixture's producer response sequence independently, save the following as `reproduce-supervisor.py` and run `python3 reproduce-supervisor.py /absolute/path/to/ida-pro-mcp` against the stated upstream checkout. This invokes its actual supervisor code with an already-exited synthetic process, disables instance discovery, and does not start a subprocess, connect to a socket or import IDA. ```python """Exercise upstream supervisor reaping with a synthetic already-exited process. No IDA modules, subprocesses, sockets or ambient discovery are used. """ import json import sys from pathlib import Path sys.path.insert(0, str(Path(sys.argv[1]).resolve() / "src")) from ida_pro_mcp import idalib_supervisor as mod class ExitedProcess: pid = 424242 def poll(self): return 0 mod._discovery.discover_instances = lambda: [] sup = mod.IdalibSupervisor(mod.McpServer("disposable-lifecycle-probe"), max_workers=1) mod.supervisor = sup session = mod.WorkerSession(session_id="fixture-session", input_path="/fixture/target", filename="target", process=ExitedProcess()) sup.sessions[session.session_id] = session sup.path_to_session[sup._path_key(session.input_path)] = session.session_id assert session.session_id in sup.sessions health = mod._handle_tools_call({"jsonrpc":"2.0", "id":1, "method":"tools/call", "params":{"name":"server_health", "arguments":{"database":session.session_id}}}) assert health["result"]["isError"] is True assert session.session_id not in sup.sessions closed = mod.idb_close(session.session_id, save=False) assert closed == {"error":"Session not found: fixture-session"} inventory = mod.idb_list() assert inventory == {"sessions":[], "count":0} assert "idapro" not in sys.modules and "idaapi" not in sys.modules print(json.dumps({"health":health,"close":closed,"inventory":inventory,"ida_modules_imported":False})) ``` The corresponding real-engine sequence was not executed: configure the documented headless supervisor, call `open_binary` with `provider_id: "ida"`, let its worker exit or expire, make an analysis request whose preliminary `server_health` call reaps and unregisters that worker, then call `close_binary` twice. The intervening request matters; idle expiration alone does not prove registry removal. Upstream's `idb_open` default minimum idle TTL is 600 seconds, with load-time adjustment in its worker lifecycle. ### Expected behavior After a completed open and a supervisor-confirmed worker release, close should verify the session and private input are absent from discovery, release the connection, remove the private workspace and succeed on retry. It must continue retaining the workspace when release cannot be verified or another worker still references that input. Live-worker close must preserve `save: false`. ### Impact and scope A previously opened headless session cannot complete cleanup after an analysis health request causes the supervisor to unregister its exited worker. Repeated close retains the private workspace and connection. Later target opens in that REA process encounter the retained pending-cleanup owner. That target-transition consequence is source-traced, rather than observed through a complete MCP/IDA workflow. ### REA version The production REA adapter was compiled from current `main` revision `693ed332b58d05192f55b8192d2c0c4bc4f4824a` and separately tested from the extracted latest published npm artifact, `rea-agents` 6.4.0. All ten compiled `dist/ida/` JavaScript files matched the published artifact byte for byte. The checks ran on macOS 27.0 arm64 with Node.js v26.10.0. The upstream supervisor checkout was revision `c133c3853faa111a9b00ee615c013b720d0c4acd`, whose project metadata names version 2.0.0. Python 3.12.13 ran the actual supervisor synthetic-process check. No licensed IDA engine, live worker, GUI database or target executable was run. ### Runtime and platform The measured runtime and platform are stated in the version field above. ### Target and artifact context The adapter fixture copies the local system Mach-O executable only to exercise input hashing and private workspace ownership. It does not execute or analyze that binary. The supervisor fixture uses an already-exited synthetic process and imports no IDA modules. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence One fresh adapter probe against each tested build returns `cleanupIncomplete: true` for both close attempts in the reaped-worker case. The private workspace remains, connection close count stays zero, and cleanup calls only `idb_close` twice. A separate inventory call reports no sessions. The live-worker control and ordinary completed open-rejection control both close successfully and remove their workspaces. Both builds produced the same failure and passing control results. The actual upstream synthetic-process check returns this sequence: ```json {"health_error":"Worker for session 'fixture-session' is not reachable","close":{"error":"Session not found: fixture-session"},"inventory":{"sessions":[],"count":0}} ``` REA parses the absent-session close error against its successful-close-only schema and returns validation errors for missing success/session/ownership fields. It does not reach `idb_list`, connection close or workspace removal. The additional claim that future target opens are blocked follows from the retained pending-cleanup owner in `BinarySession`; that public-session consequence was source-traced rather than run end to end. - Expected source: `docs/ida-provider.md`, headless lifecycle and cleanup paragraphs, promises verified release followed by private workspace removal; `AGENTS.md` requires retaining resources when cleanup remains uncertain. - Failure source: The inline production-adapter probe observes repeated failed close with empty inventory and a retained workspace; the inline upstream check confirms the exact error responses. `src/ida/IdaSessionClient.ts:412-479` reaches inventory only after a successful stale-ID close, and `src/application/binary/BinarySession.ts:246-247,408-434` retains and retries the failed owner. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete Evidence provenance details: observed production-adapter boundary failure and actual upstream supervisor behavior with a synthetic exited process; the real idle-expiration prerequisite and public pending-open consequence are source-derived. Local verification details: reproduced once against each tested build with disposable fixtures, plus one actual upstream supervisor synthetic-process check. No real IDA engine or complete CLI/MCP engine workflow was run. Reproduction completeness details: complete for the adapter failure and supervisor response sequence reproduced above. Pinned source breadcrumbs: [REA close path](https://github.com/morluto/rea/blob/13e03e94363a007353fcb5dfedec764de12fc927/src/ida/IdaSessionClient.ts#L412), [pending cleanup](https://github.com/morluto/rea/blob/13e03e94363a007353fcb5dfedec764de12fc927/src/application/binary/BinarySession.ts#L246), and [upstream reap/close behavior](https://github.com/mrexodia/ida-pro-mcp/blob/c133c3853faa111a9b00ee615c013b720d0c4acd/src/ida_pro_mcp/idalib_supervisor.py#L1053). ### Typed outcome or recovery expected Repeat the reaped-worker probe and require both close calls to succeed, the private workspace to be absent, and the connection to close once after verified empty discovery. Exercise the public session transition and require a later target open to proceed after that cleanup. Preserve the passing live-worker and completed-open-rejection controls. Add a discriminating case where inventory is unavailable or still contains a worker using the private input; that case must continue reporting incomplete cleanup and retaining the workspace. ### Triage assessment - Impact level: P1 - Assessment status: provisional - Impact basis: Cleanup failure leaves a pending owner that source inspection shows blocking later target opens in the same REA process, preventing the core target-transition workflow with no verified practical recovery. The cleanup failure was directly observed at production boundaries with synthetic fixtures, while real-engine incidence and practical recovery remain unmeasured. The live-worker and completed-open-rejection controls pass. - Workaround status: unverified - Workaround basis: Restarting REA should clear its process-local pending owner, followed by manual cleanup only after independently verifying upstream inventory absence. That complete recovery was not exercised against a real IDA engine, so its practical availability and safety are not established here.
bsakaguchi 2026-10-15 18:26:18 +00:00
Author
Owner

@N0zoM1z0 commented on GitHub (Oct 11, 2026):

Replayed the issue’s production IdaSessionClient connection-boundary reproduction against merged main 844fa52772c1533f7358a71a764da780503775a3. The expired-worker case still fails both closes and retains its workspace/connection; the live-worker and completed-open-rejection controls pass. This verifies the adapter failure with an injected connection, without a live IDA engine or a complete CLI/MCP provider workflow.

There is a protocol constraint on a safe recovery: pinned upstream c133c3853faa111a9b00ee615c013b720d0c4acd and current upstream e65963e531228889801805e6e744f0698755f053 emit the exact not-reachable error only after the owned process is already exited or its termination/kill wait completes. However, the MCP response exposes that only as error text; REA receives a generic protocol error. Ordinary idb_close failure plus an empty idb_list cannot establish release because close unregisters before terminating, and discovery failure can be represented as empty inventory.

A robust fix needs an idempotent close/release receipt identifying the exact session and private input, owned=true, backend=worker, and confirmed process exit; inventory used as proof also needs an explicit completed/failed discovery status. REA can validate that receipt against its original owned identity and retain resources on ambiguity. I am keeping the issue open rather than weakening successful-close validation or treating absent inventory after any protocol error as cleanup success.

<!-- gh-comment-id:6106475291 --> @N0zoM1z0 commented on GitHub (Oct 11, 2026): Replayed the issue’s production `IdaSessionClient` connection-boundary reproduction against merged main `844fa52772c1533f7358a71a764da780503775a3`. The expired-worker case still fails both closes and retains its workspace/connection; the live-worker and completed-open-rejection controls pass. This verifies the adapter failure with an injected connection, without a live IDA engine or a complete CLI/MCP provider workflow. There is a protocol constraint on a safe recovery: pinned upstream `c133c3853faa111a9b00ee615c013b720d0c4acd` and current upstream `e65963e531228889801805e6e744f0698755f053` emit the exact not-reachable error only after the owned process is already exited or its termination/kill wait completes. However, the MCP response exposes that only as error text; REA receives a generic protocol error. Ordinary `idb_close` failure plus an empty `idb_list` cannot establish release because close unregisters before terminating, and discovery failure can be represented as empty inventory. A robust fix needs an idempotent close/release receipt identifying the exact session and private input, `owned=true`, `backend=worker`, and confirmed process exit; inventory used as proof also needs an explicit completed/failed discovery status. REA can validate that receipt against its original owned identity and retain resources on ambiguity. I am keeping the issue open rather than weakening successful-close validation or treating absent inventory after any protocol error as cleanup success.
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#572
No description provided.