[GH-ISSUE #5335] [Bug] Failed start_gdb_session retains a cleanup session that reports ready and accepts gdb_console commands #3243

Closed
opened 2026-10-31 00:38:15 +00:00 by bsakaguchi · 1 comment
Owner

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

Before submitting

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

Area

Process capture or runtime observation

Observed behavior

When start_gdb_session fails a mandatory startup check after GDB's first prompt, and the owned debugger process cannot yet be confirmed stopped, the tool returns cleanup_incomplete and retains the session ID in error.details.resources. For that retained ID, gdb_session_status reports state: "ready", and gdb_console accepts show version and returns a complete MI result. The start error's own error.details.partial_observation Evidence records "No usable debugger session became ready during GDB startup."

Two startup checks reproduce it: GDB rejecting the required -gdb-set auto-load off, and an inferior_tty readback that does not match the requested terminal. A startup-command timeout under the same cleanup condition is handled correctly: status reports closed and gdb_console returns invalid_request.

This was observed through the production MCP registration (registerGdbTools, GdbSessionManager, MI parser, error projection) with a synthetic MI peer, not a real GDB. The peer is a small Node script that prints valid MI prompt and result records. The cleanup failure is injected: the probe's stopProcess hook returns {status: "incomplete"} for the first stop attempt (the first two in the timeout control) and then delegates to the real stopOwnedDebuggerProcess. The probe omits the optional evidence recorder and binary-session admission arguments that createServer passes to registerGdbTools; the start, status and console code paths it exercises are otherwise the production ones. No GDB, inferior, executable target or terminal was used.

Reproduction

From a fresh clone at the revision under REA version, run npm ci && npm run build:cached. Create a disposable workspace:

WORK=$(mktemp -d)
mkdir "$WORK/home" "$WORK/tmp"

Save this as $WORK/probe.mts:

import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import { writeFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
const checkout = process.cwd();
const work = process.env.REA_WORK!;
const require = createRequire(join(checkout, 'package.json'));
const { Client, InMemoryTransport } = await import(require.resolve('@modelcontextprotocol/client'));
const { McpServer } = await import(require.resolve('@modelcontextprotocol/server'));
const load = (p: string) => import(pathToFileURL(join(checkout, 'src', p + '.ts')).href);
const { GdbSessionManager } = await load('gdb/GdbSessionManager');
const { stopOwnedDebuggerProcess } = await load('process/ownedDebuggerSession');
const { registerGdbTools } = await load('server/registerGdbTools');
const { createToolResultDelivery } = await load('server/toolResult');
const { silentLogger } = await load('logger');
for (const mode of ['autoload-reject', 'tty-mismatch', 'timeout', 'success', 'confirmed-stop']) {
  const provider = join(work, 'fake-gdb.mjs');
  await writeFile(provider, `#!${process.execPath}
import { createInterface } from 'node:readline';
const mode = ${JSON.stringify(mode)};
process.stdout.write('(gdb)\\n');
createInterface({input:process.stdin}).on('line', line => {
 const token = /^(\\d+)/.exec(line)?.[1];
 if (!token) return;
 const command = line.slice(token.length);
 if(mode === 'timeout' && command === '-gdb-set auto-load off') return;
 const result = (mode === 'autoload-reject' || mode === 'confirmed-stop') && command === '-gdb-set auto-load off'
   ? 'error,msg="fixture rejects auto-load"'
   : command === '-inferior-tty-show' ? 'done,inferior_tty_terminal="/dev/pts/8"'
   : command === '-list-thread-groups --recurse 1' ? 'done,groups=[]'
   : 'done';
 process.stdout.write(token + '^' + result + '\\n(gdb)\\n');
 if(command === '-gdb-exit') process.exit(0);
});
`, {mode:0o700});
  let stopCalls = 0;
  const manager = new GdbSessionManager({
    platform:'linux', environment:{...process.env,REA_GDB_COMMAND:provider},
    startupTimeoutMs:2000, commandTimeoutMs:100,
    stopProcess: async (session: any, name: string) => {
      stopCalls += 1;
      return stopCalls <= (mode === 'timeout' ? 2 : 1) && mode !== 'success' && mode !== 'confirmed-stop'
        ? {status:'incomplete',reason:'fixture stop uncertainty'}
        : stopOwnedDebuggerProcess(session,name);
    },
  });
  const server = new McpServer({name:'gdb-startup-check',version:'1'});
  registerGdbTools(server,manager,silentLogger,createToolResultDelivery(undefined));
  const client = new Client({name:'gdb-startup-check',version:'1'});
  const [ct,st] = InMemoryTransport.createLinkedPair();
  const call = (name:string,args:Record<string,unknown>) => client.callTool({name,arguments:args});
  try {
    await server.connect(st); await client.connect(ct);
    const started = await call('start_gdb_session',mode === 'tty-mismatch' ? {inferior_tty:'/dev/pts/7'} : {});
    const data = started.structuredContent as any;
    if(mode === 'confirmed-stop') {
      assert(started.isError); assert.equal(data.error.details.cleanup,undefined);
      assert.equal(stopCalls,2);
      console.log(JSON.stringify({mode,start_error:data.error.code,stopCalls,cleanup:'not retained'}));
      continue;
    }
    const sid = mode === 'success' ? data.session_id : data.error.details.resources[0];
    const status = await call('gdb_session_status',{session_id:sid});
    const command = await call('gdb_console',{session_id:sid,command:'show version'});
    const state = (status.structuredContent as any).state;
    const error = (command.structuredContent as any)?.error;
    if(mode === 'timeout') {
      assert(started.isError); assert.equal(state,'closed'); assert(command.isError);
      assert.equal(error.code,'invalid_request');
    } else {
      assert.equal(state,'ready'); assert(!command.isError);
      assert.equal(!!started.isError,mode !== 'success');
    }
    console.log(JSON.stringify({mode,start_error:data.error?.code,start_reason:data.error?.details?.reason,
      retained:mode !== 'success',state,inferior_tty_verified:(status.structuredContent as any).inferior_tty_verified,
      console_isError:!!command.isError,console_error:error?.code,
      console_result:(command.structuredContent as any)?.normalized_result ?? (command.structuredContent as any)?.result,
      stopCallsBeforeClose:stopCalls}));
    const closed=await call('close_gdb_session',{session_id:sid});
    assert(!closed.isError); assert.equal(manager.status(sid),undefined);
    console.log(JSON.stringify({mode,recovery:'same-ID close succeeded',stopCalls}));
  } finally {
    await manager.closeAll(); await client.close(); await server.close();
    await rm(provider,{force:true});
  }
}
console.log(JSON.stringify({runtime:process.version,platform:process.platform,arch:process.arch,result:'PASS'}));

Run it from the checkout root:

env -i HOME="$WORK/home" PATH="$PATH" XDG_CONFIG_HOME="$WORK/config" XDG_CACHE_HOME="$WORK/cache" XDG_DATA_HOME="$WORK/data" XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/tmp" REA_WORK="$WORK" node --import "$PWD/node_modules/tsx/dist/loader.mjs" "$WORK/probe.mts"

The probe runs five cases sequentially and asserts the observed behavior, so PASS means the defect reproduced:

  • autoload-reject: the peer answers -gdb-set auto-load off with ^error; the first stop attempt is reported incomplete.
  • tty-mismatch: the caller requests inferior_tty: "/dev/pts/7" and the peer reports /dev/pts/8; the first stop attempt is reported incomplete. These are fixture strings; no terminal is opened.
  • timeout (control): the peer never answers the auto-load command; both internal stop attempts are reported incomplete.
  • success (control): normal startup.
  • confirmed-stop (control): the auto-load command is rejected and cleanup is not faked.

Each case closes its retained session with the same ID and stops its peer. Delete $WORK afterward. The probe passed in each of three runs (one per revision listed under REA version).

Expected behavior

docs/reverse-engineering-tools.md says MCP startup disables auto-loading and that REA configures an optional inferior_tty "before the session becomes ready and checks the exact value with GDB/MI". The start_gdb_session contract in src/contracts/gdbToolContracts.ts (lines 31-37) says the same. docs/tool-design.md keeps provider readiness separate from retained resource ownership and says to "derive readiness from the current startup's completion".

When either startup check fails and cleanup cannot yet be confirmed, the retained ID should work as a cleanup handle only: status should report closed (or otherwise unavailable), gdb_console should reject ordinary commands without sending them to GDB, and close_gdb_session should still be able to retry cleanup with the same ID. The startup-command timeout path already behaves this way. A successful start must still report ready and accept commands.

Impact and scope

A caller gets a failed start alongside a session that reports ready and accepts arbitrary console commands, even though auto-loading was not disabled, or the requested inferior TTY was not confirmed. Commands then run in a GDB whose required configuration did not complete.

The trigger needs two conditions together: a startup check failure after the prompt, and an owned-process stop that cannot be confirmed. Production cleanup can return incomplete when exit cannot be confirmed after direct termination attempts (src/process/ownedDebuggerSession.ts). How often that happens with a real GDB was not measured, and neither were downstream target effects.

Workaround: treat the ID returned by a failed start only as a cleanup handle, and retry close_gdb_session with it. Same-ID close removed the retained session in both failing cases.

No security boundary crossing, target execution or data loss was observed. GDB sessions are Linux-only; the probe selects the Linux code path explicitly.

REA version

Source main 5fa4defcbc0e55ef1ec84c888b918abe945303fe. Also reproduced at main 833136507dd79d959340b09862a53af344e4f1b3 and at the rea-agents-6.7.0 tag source (27dc42d10249f0410904a75c0997842eb03b931c). The published npm package was not executed.

Runtime and platform

Node v24.22.0 (current main and 6.7.0 tag runs) and v26.12.0 (earlier main run); macOS 27.0.1 arm64, using GdbSessionManager's explicit platform: "linux" option with a synthetic MI peer. The pinned Node 24.18.0 was not used.

Target and artifact context

Not applicable. No target executable or inferior; the GDB provider is a synthetic Node MI peer.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Probe output on current main (Node v24.22.0), with the long console_result objects shortened to their MI record:

{"mode":"autoload-reject","start_error":"cleanup_incomplete","start_reason":"GDB startup failed (Provider gdb cannot execute start_gdb_session: GDB rejected the required automatic-loading disable command (1^error,msg=\"fixture rejects auto-load\"); the session was not started.) and its owned process could not be confirmed stopped: fixture stop uncertainty","retained":true,"state":"ready","inferior_tty_verified":false,"console_isError":false,"console_result":{"command":"show version","mi":"2^done",...,"completion_status":"complete"},"stopCallsBeforeClose":1}
{"mode":"autoload-reject","recovery":"same-ID close succeeded","stopCalls":2}
{"mode":"tty-mismatch","start_error":"cleanup_incomplete","start_reason":"GDB startup failed (Provider gdb cannot execute start_gdb_session: GDB did not report the exact caller-selected inferior TTY after configuration.) and its owned process could not be confirmed stopped: fixture stop uncertainty","retained":true,"state":"ready","inferior_tty_verified":false,"console_isError":false,"console_result":{"command":"show version","mi":"4^done",...,"completion_status":"complete"},"stopCallsBeforeClose":1}
{"mode":"tty-mismatch","recovery":"same-ID close succeeded","stopCalls":2}
{"mode":"timeout","start_error":"cleanup_incomplete","retained":true,"state":"closed","inferior_tty_verified":false,"console_isError":true,"console_error":"invalid_request","stopCallsBeforeClose":2}
{"mode":"timeout","recovery":"same-ID close succeeded","stopCalls":3}
{"mode":"success","retained":false,"state":"ready","inferior_tty_verified":false,"console_isError":false,"console_result":{"command":"show version","mi":"2^done",...,"completion_status":"complete"},"stopCallsBeforeClose":0}
{"mode":"success","recovery":"same-ID close succeeded","stopCalls":1}
{"mode":"confirmed-stop","start_error":"capability_unavailable","stopCalls":2,"cleanup":"not retained"}
{"runtime":"v24.22.0","platform":"darwin","arch":"arm64","result":"PASS"}

In the MCP result, the failed start carries error.code: "cleanup_incomplete", error.details.execution_failure: "capability_unavailable", and the retained ID in error.details.resources. Same-ID close succeeded for every retained session once cleanup was allowed. The 6.7.0 tag run printed the same results.

Typed outcome or recovery expected

start_gdb_session should still return cleanup_incomplete with the startup failure, partial observation and retained resource ID. For that ID, gdb_session_status should report state: "closed", and gdb_console should return an error (as the timeout control's invalid_request does) without sending the command. close_gdb_session with the same ID should retry cleanup and remove the session once the process stop is confirmed.

Analysis or suspected cause

Source-derived, at the current main revision:

  • GdbSessionManager.ts line 811 sets session.ready = true right after the first prompt, before the auto-load and inferior-TTY checks run.
  • The startup catch block (lines 980-1007) retains the session in #sessions when stop is incomplete but leaves ready set. It also does not add the failure to session.cleanupFailures, the set that retryCleanupFailures (the cleanup-retry hook registerGdbTools passes for gdb_console) walks.
  • status (lines 1239-1245) and the gdb_console guard (lines 1064-1080) treat a session as usable when it is ready, not exited, and has no protocol error, so they admit it.

The timeout control reports closed because a command timeout sets session.protocolError (line 1884). An MI ^error result or a mismatched TTY readback completes the command normally, so protocolError stays unset, and ready is never reset after line 811.

Additional context

Related but different: #5099 (TTY verification revoked after a successful start), #4653 (close-precondition classification for an initialized session), and the lifecycle audit #4227. Open PR #1961 changes GDB console, interrupt and cleanup ownership, but its current diff leaves the post-prompt readiness assignment and this retention branch unchanged. Cancellation during startup was not tested.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5335 ### 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 Process capture or runtime observation ### Observed behavior When `start_gdb_session` fails a mandatory startup check after GDB's first prompt, and the owned debugger process cannot yet be confirmed stopped, the tool returns `cleanup_incomplete` and retains the session ID in `error.details.resources`. For that retained ID, `gdb_session_status` reports `state: "ready"`, and `gdb_console` accepts `show version` and returns a complete MI result. The start error's own `error.details.partial_observation` Evidence records "No usable debugger session became ready during GDB startup." Two startup checks reproduce it: GDB rejecting the required `-gdb-set auto-load off`, and an `inferior_tty` readback that does not match the requested terminal. A startup-command timeout under the same cleanup condition is handled correctly: status reports `closed` and `gdb_console` returns `invalid_request`. This was observed through the production MCP registration (`registerGdbTools`, `GdbSessionManager`, MI parser, error projection) with a synthetic MI peer, not a real GDB. The peer is a small Node script that prints valid MI prompt and result records. The cleanup failure is injected: the probe's `stopProcess` hook returns `{status: "incomplete"}` for the first stop attempt (the first two in the timeout control) and then delegates to the real `stopOwnedDebuggerProcess`. The probe omits the optional evidence recorder and binary-session admission arguments that `createServer` passes to `registerGdbTools`; the start, status and console code paths it exercises are otherwise the production ones. No GDB, inferior, executable target or terminal was used. ### Reproduction From a fresh clone at the revision under REA version, run `npm ci && npm run build:cached`. Create a disposable workspace: ```sh WORK=$(mktemp -d) mkdir "$WORK/home" "$WORK/tmp" ``` Save this as `$WORK/probe.mts`: ```typescript import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; import { writeFile, rm } from 'node:fs/promises'; import { join } from 'node:path'; const checkout = process.cwd(); const work = process.env.REA_WORK!; const require = createRequire(join(checkout, 'package.json')); const { Client, InMemoryTransport } = await import(require.resolve('@modelcontextprotocol/client')); const { McpServer } = await import(require.resolve('@modelcontextprotocol/server')); const load = (p: string) => import(pathToFileURL(join(checkout, 'src', p + '.ts')).href); const { GdbSessionManager } = await load('gdb/GdbSessionManager'); const { stopOwnedDebuggerProcess } = await load('process/ownedDebuggerSession'); const { registerGdbTools } = await load('server/registerGdbTools'); const { createToolResultDelivery } = await load('server/toolResult'); const { silentLogger } = await load('logger'); for (const mode of ['autoload-reject', 'tty-mismatch', 'timeout', 'success', 'confirmed-stop']) { const provider = join(work, 'fake-gdb.mjs'); await writeFile(provider, `#!${process.execPath} import { createInterface } from 'node:readline'; const mode = ${JSON.stringify(mode)}; process.stdout.write('(gdb)\\n'); createInterface({input:process.stdin}).on('line', line => { const token = /^(\\d+)/.exec(line)?.[1]; if (!token) return; const command = line.slice(token.length); if(mode === 'timeout' && command === '-gdb-set auto-load off') return; const result = (mode === 'autoload-reject' || mode === 'confirmed-stop') && command === '-gdb-set auto-load off' ? 'error,msg="fixture rejects auto-load"' : command === '-inferior-tty-show' ? 'done,inferior_tty_terminal="/dev/pts/8"' : command === '-list-thread-groups --recurse 1' ? 'done,groups=[]' : 'done'; process.stdout.write(token + '^' + result + '\\n(gdb)\\n'); if(command === '-gdb-exit') process.exit(0); }); `, {mode:0o700}); let stopCalls = 0; const manager = new GdbSessionManager({ platform:'linux', environment:{...process.env,REA_GDB_COMMAND:provider}, startupTimeoutMs:2000, commandTimeoutMs:100, stopProcess: async (session: any, name: string) => { stopCalls += 1; return stopCalls <= (mode === 'timeout' ? 2 : 1) && mode !== 'success' && mode !== 'confirmed-stop' ? {status:'incomplete',reason:'fixture stop uncertainty'} : stopOwnedDebuggerProcess(session,name); }, }); const server = new McpServer({name:'gdb-startup-check',version:'1'}); registerGdbTools(server,manager,silentLogger,createToolResultDelivery(undefined)); const client = new Client({name:'gdb-startup-check',version:'1'}); const [ct,st] = InMemoryTransport.createLinkedPair(); const call = (name:string,args:Record<string,unknown>) => client.callTool({name,arguments:args}); try { await server.connect(st); await client.connect(ct); const started = await call('start_gdb_session',mode === 'tty-mismatch' ? {inferior_tty:'/dev/pts/7'} : {}); const data = started.structuredContent as any; if(mode === 'confirmed-stop') { assert(started.isError); assert.equal(data.error.details.cleanup,undefined); assert.equal(stopCalls,2); console.log(JSON.stringify({mode,start_error:data.error.code,stopCalls,cleanup:'not retained'})); continue; } const sid = mode === 'success' ? data.session_id : data.error.details.resources[0]; const status = await call('gdb_session_status',{session_id:sid}); const command = await call('gdb_console',{session_id:sid,command:'show version'}); const state = (status.structuredContent as any).state; const error = (command.structuredContent as any)?.error; if(mode === 'timeout') { assert(started.isError); assert.equal(state,'closed'); assert(command.isError); assert.equal(error.code,'invalid_request'); } else { assert.equal(state,'ready'); assert(!command.isError); assert.equal(!!started.isError,mode !== 'success'); } console.log(JSON.stringify({mode,start_error:data.error?.code,start_reason:data.error?.details?.reason, retained:mode !== 'success',state,inferior_tty_verified:(status.structuredContent as any).inferior_tty_verified, console_isError:!!command.isError,console_error:error?.code, console_result:(command.structuredContent as any)?.normalized_result ?? (command.structuredContent as any)?.result, stopCallsBeforeClose:stopCalls})); const closed=await call('close_gdb_session',{session_id:sid}); assert(!closed.isError); assert.equal(manager.status(sid),undefined); console.log(JSON.stringify({mode,recovery:'same-ID close succeeded',stopCalls})); } finally { await manager.closeAll(); await client.close(); await server.close(); await rm(provider,{force:true}); } } console.log(JSON.stringify({runtime:process.version,platform:process.platform,arch:process.arch,result:'PASS'})); ``` Run it from the checkout root: ```sh env -i HOME="$WORK/home" PATH="$PATH" XDG_CONFIG_HOME="$WORK/config" XDG_CACHE_HOME="$WORK/cache" XDG_DATA_HOME="$WORK/data" XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/tmp" REA_WORK="$WORK" node --import "$PWD/node_modules/tsx/dist/loader.mjs" "$WORK/probe.mts" ``` The probe runs five cases sequentially and asserts the observed behavior, so `PASS` means the defect reproduced: - `autoload-reject`: the peer answers `-gdb-set auto-load off` with `^error`; the first stop attempt is reported incomplete. - `tty-mismatch`: the caller requests `inferior_tty: "/dev/pts/7"` and the peer reports `/dev/pts/8`; the first stop attempt is reported incomplete. These are fixture strings; no terminal is opened. - `timeout` (control): the peer never answers the auto-load command; both internal stop attempts are reported incomplete. - `success` (control): normal startup. - `confirmed-stop` (control): the auto-load command is rejected and cleanup is not faked. Each case closes its retained session with the same ID and stops its peer. Delete `$WORK` afterward. The probe passed in each of three runs (one per revision listed under REA version). ### Expected behavior [`docs/reverse-engineering-tools.md`](https://github.com/morluto/rea/blob/5fa4defcbc0e55ef1ec84c888b918abe945303fe/docs/reverse-engineering-tools.md#L34) says MCP startup disables auto-loading and that REA configures an optional `inferior_tty` "before the session becomes ready and checks the exact value with GDB/MI". The `start_gdb_session` contract in `src/contracts/gdbToolContracts.ts` (lines 31-37) says the same. [`docs/tool-design.md`](https://github.com/morluto/rea/blob/5fa4defcbc0e55ef1ec84c888b918abe945303fe/docs/tool-design.md#L78-L100) keeps provider readiness separate from retained resource ownership and says to "derive readiness from the current startup's completion". When either startup check fails and cleanup cannot yet be confirmed, the retained ID should work as a cleanup handle only: status should report `closed` (or otherwise unavailable), `gdb_console` should reject ordinary commands without sending them to GDB, and `close_gdb_session` should still be able to retry cleanup with the same ID. The startup-command timeout path already behaves this way. A successful start must still report `ready` and accept commands. ### Impact and scope A caller gets a failed start alongside a session that reports ready and accepts arbitrary console commands, even though auto-loading was not disabled, or the requested inferior TTY was not confirmed. Commands then run in a GDB whose required configuration did not complete. The trigger needs two conditions together: a startup check failure after the prompt, and an owned-process stop that cannot be confirmed. Production cleanup can return incomplete when exit cannot be confirmed after direct termination attempts ([`src/process/ownedDebuggerSession.ts`](https://github.com/morluto/rea/blob/5fa4defcbc0e55ef1ec84c888b918abe945303fe/src/process/ownedDebuggerSession.ts#L252-L266)). How often that happens with a real GDB was not measured, and neither were downstream target effects. Workaround: treat the ID returned by a failed start only as a cleanup handle, and retry `close_gdb_session` with it. Same-ID close removed the retained session in both failing cases. No security boundary crossing, target execution or data loss was observed. GDB sessions are Linux-only; the probe selects the Linux code path explicitly. ### REA version Source `main` `5fa4defcbc0e55ef1ec84c888b918abe945303fe`. Also reproduced at `main` `833136507dd79d959340b09862a53af344e4f1b3` and at the `rea-agents-6.7.0` tag source (`27dc42d10249f0410904a75c0997842eb03b931c`). The published npm package was not executed. ### Runtime and platform Node v24.22.0 (current `main` and 6.7.0 tag runs) and v26.12.0 (earlier `main` run); macOS 27.0.1 arm64, using `GdbSessionManager`'s explicit `platform: "linux"` option with a synthetic MI peer. The pinned Node 24.18.0 was not used. ### Target and artifact context Not applicable. No target executable or inferior; the GDB provider is a synthetic Node MI peer. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence Probe output on current `main` (Node v24.22.0), with the long `console_result` objects shortened to their MI record: ```text {"mode":"autoload-reject","start_error":"cleanup_incomplete","start_reason":"GDB startup failed (Provider gdb cannot execute start_gdb_session: GDB rejected the required automatic-loading disable command (1^error,msg=\"fixture rejects auto-load\"); the session was not started.) and its owned process could not be confirmed stopped: fixture stop uncertainty","retained":true,"state":"ready","inferior_tty_verified":false,"console_isError":false,"console_result":{"command":"show version","mi":"2^done",...,"completion_status":"complete"},"stopCallsBeforeClose":1} {"mode":"autoload-reject","recovery":"same-ID close succeeded","stopCalls":2} {"mode":"tty-mismatch","start_error":"cleanup_incomplete","start_reason":"GDB startup failed (Provider gdb cannot execute start_gdb_session: GDB did not report the exact caller-selected inferior TTY after configuration.) and its owned process could not be confirmed stopped: fixture stop uncertainty","retained":true,"state":"ready","inferior_tty_verified":false,"console_isError":false,"console_result":{"command":"show version","mi":"4^done",...,"completion_status":"complete"},"stopCallsBeforeClose":1} {"mode":"tty-mismatch","recovery":"same-ID close succeeded","stopCalls":2} {"mode":"timeout","start_error":"cleanup_incomplete","retained":true,"state":"closed","inferior_tty_verified":false,"console_isError":true,"console_error":"invalid_request","stopCallsBeforeClose":2} {"mode":"timeout","recovery":"same-ID close succeeded","stopCalls":3} {"mode":"success","retained":false,"state":"ready","inferior_tty_verified":false,"console_isError":false,"console_result":{"command":"show version","mi":"2^done",...,"completion_status":"complete"},"stopCallsBeforeClose":0} {"mode":"success","recovery":"same-ID close succeeded","stopCalls":1} {"mode":"confirmed-stop","start_error":"capability_unavailable","stopCalls":2,"cleanup":"not retained"} {"runtime":"v24.22.0","platform":"darwin","arch":"arm64","result":"PASS"} ``` In the MCP result, the failed start carries `error.code: "cleanup_incomplete"`, `error.details.execution_failure: "capability_unavailable"`, and the retained ID in `error.details.resources`. Same-ID close succeeded for every retained session once cleanup was allowed. The 6.7.0 tag run printed the same results. ### Typed outcome or recovery expected `start_gdb_session` should still return `cleanup_incomplete` with the startup failure, partial observation and retained resource ID. For that ID, `gdb_session_status` should report `state: "closed"`, and `gdb_console` should return an error (as the timeout control's `invalid_request` does) without sending the command. `close_gdb_session` with the same ID should retry cleanup and remove the session once the process stop is confirmed. ### Analysis or suspected cause Source-derived, at the current `main` revision: - [`GdbSessionManager.ts` line 811](https://github.com/morluto/rea/blob/5fa4defcbc0e55ef1ec84c888b918abe945303fe/src/gdb/GdbSessionManager.ts#L811) sets `session.ready = true` right after the first prompt, before the auto-load and inferior-TTY checks run. - The startup catch block ([lines 980-1007](https://github.com/morluto/rea/blob/5fa4defcbc0e55ef1ec84c888b918abe945303fe/src/gdb/GdbSessionManager.ts#L980-L1007)) retains the session in `#sessions` when stop is incomplete but leaves `ready` set. It also does not add the failure to `session.cleanupFailures`, the set that `retryCleanupFailures` (the cleanup-retry hook `registerGdbTools` passes for `gdb_console`) walks. - `status` ([lines 1239-1245](https://github.com/morluto/rea/blob/5fa4defcbc0e55ef1ec84c888b918abe945303fe/src/gdb/GdbSessionManager.ts#L1239-L1245)) and the `gdb_console` guard ([lines 1064-1080](https://github.com/morluto/rea/blob/5fa4defcbc0e55ef1ec84c888b918abe945303fe/src/gdb/GdbSessionManager.ts#L1064-L1080)) treat a session as usable when it is `ready`, not exited, and has no protocol error, so they admit it. The timeout control reports `closed` because a command timeout sets `session.protocolError` ([line 1884](https://github.com/morluto/rea/blob/5fa4defcbc0e55ef1ec84c888b918abe945303fe/src/gdb/GdbSessionManager.ts#L1884)). An MI `^error` result or a mismatched TTY readback completes the command normally, so `protocolError` stays unset, and `ready` is never reset after line 811. ### Additional context Related but different: #5099 (TTY verification revoked after a successful start), #4653 (close-precondition classification for an initialized session), and the lifecycle audit #4227. Open PR #1961 changes GDB console, interrupt and cleanup ownership, but its current diff leaves the post-prompt readiness assignment and this retention branch unchanged. Cancellation during startup was not tested.
Author
Owner

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

Duplicate of #5331, which was filed a few minutes earlier for the same defect. This report adds a startup-timeout control (status reports closed and gdb_console returns invalid_request), which shows that ready is never reset after the readiness assignment and that only protocolError keeps the timeout path closed.

<!-- gh-comment-id:6281745794 --> @coygeek commented on GitHub (Oct 20, 2026): Duplicate of #5331, which was filed a few minutes earlier for the same defect. This report adds a startup-timeout control (status reports `closed` and `gdb_console` returns `invalid_request`), which shows that `ready` is never reset after the readiness assignment and that only `protocolError` keeps the timeout path closed.
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#3243
No description provided.