[GH-ISSUE #6110] [Bug] Dart AOT admission drops the exact caller cancellation reason #3742

Closed
opened 2026-10-31 00:43:35 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

MCP or agent integration

Observed behavior

The production inspectDartAot application wrapper returns AnalysisCancelledError with cause === undefined for an already-observed abort. The exact same signal passed directly to FlutterBuildAnalysisService.execute retains its reason. The provider's own early guard also drops it. Null, a primitive string and an object reason all reproduced the loss.

A controlled queued abort after real session admission succeeded reached the wrapper with the signal aborted and reproduced the same missing cause. This is an application/session-boundary reproduction, not an SDK or stdio cancellation test.

Reproduction

Use a prepared checkout with the source revision below and locked dependencies. Save the following as src/application/cancellationReason.repro.test.ts, then run only that file with the repository-pinned Node.js 24.18.0:

node node_modules/vitest/vitest.mjs run src/application/cancellationReason.repro.test.ts --maxWorkers 1

The assertions characterize the current behavior, so a passing test demonstrates the defect and its controls. Remove the test after running it.

import { writeFile, mkdtemp, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { expect, it } from 'vitest';
import { inspectDartAot } from './flutter/DartAotInspection.js';
import { FlutterBuildAnalysisService } from './flutter/FlutterBuildAnalysisService.js';
import { FlutterBuildProvider } from '../flutter/FlutterBuildProvider.js';
import { inspectAnalysisViewWithFiles } from './analysisView/AnalysisViewService.js';
import { readEvidenceFileSource } from './analysisView/evidenceFileSource.js';
import { BinarySession } from './binary/BinarySession.js';
import { InvestigationRecords } from './investigation/InvestigationRecords.js';
import { createTestProviderRouter } from '../../tests/fixtures/binarySession.js';
import { createAnalysisExecution, type AnalysisClient } from './AnalysisProvider.js';
import { ProviderAdapterError } from '../domain/providerAdapterError.js';
import { createAnalysisProfile } from '../domain/analysisProfile.js';
import { elf } from '../domain/binaryTarget.fixture.js';
import { ok, err } from '../domain/result.js';

it.each([null, 'caller stop', { phase: 'admission' }])('Flutter early cancellation loses exact reason %j', async reason => {
  const controller = new AbortController(); controller.abort(reason);
  const provider = new FlutterBuildProvider();
  const service = new FlutterBuildAnalysisService(provider);
  const input = { path: '/targets/unopened.apk' };
  const options = { signal: controller.signal };
  const control = await service.execute('inspect_dart_aot', input, options);
  const app = await inspectDartAot(service, input, {}, options);
  const direct = await provider.execute({ operation: 'inspect_dart_aot', input }, options);
  expect(control.ok).toBe(false); expect(app.ok).toBe(false); expect(direct.ok).toBe(false);
  if (control.ok || app.ok || direct.ok) throw new Error('Expected cancellation');
  expect(control.error.cause).toBe(reason);
  expect(app.error._tag).toBe('AnalysisCancelledError');
  expect(app.error.cause).toBeUndefined();
  expect(direct.error.cause).toBeUndefined();
  console.log('FLUTTER', JSON.stringify({ reason, serviceExact: control.error.cause === reason, applicationExact: app.error.cause === reason, providerExact: direct.error.cause === reason }));
  await provider.close();
});


it('Flutter wrapper can observe abort after real session admission succeeds', async () => {
  const session = new BinarySession(createTestProviderRouter(() => { throw new Error('must not acquire'); }));
  const provider = new FlutterBuildProvider();
  const service = new FlutterBuildAnalysisService(provider);
  const controller = new AbortController(); const reason = { phase: 'after-admission' };
  let reached = false; let sawAborted = false;
  const running = session.withAdmittedAnalysis('inspect_dart_aot', controller.signal, async () => {
    reached = true; sawAborted = controller.signal.aborted;
    return inspectDartAot(service, { path: '/targets/unopened.apk' }, {}, { signal: controller.signal });
  });
  queueMicrotask(() => controller.abort(reason));
  try {
    const admitted = await running;
    expect(admitted.ok).toBe(true);
    if (!admitted.ok) throw admitted.error;
    expect(reached).toBe(true); expect(sawAborted).toBe(true);
    expect(admitted.value.ok).toBe(false);
    if (admitted.value.ok) throw new Error('Expected cancellation');
    expect(admitted.value.error._tag).toBe('AnalysisCancelledError');
    expect(admitted.value.error.cause).toBeUndefined();
    console.log('FLUTTER_ADMISSION', JSON.stringify({ reached, sawAborted, tag: admitted.value.error._tag, exactReason: admitted.value.error.cause === reason }));
  } finally { await session.close(); await provider.close(); }
});

Expected behavior

Keep AnalysisCancelledError.cause === signal.reason exactly, including null, primitives and object identity. The lifecycle contract requires this, and the shared service already does so at its admission boundary.

Impact and scope

P2: the affected typed-result boundary loses required cancellation provenance; successful analysis and the cancellation category are unchanged. Code retaining the original signal can still read the reason there. No changed MCP wire field, input corruption or persistent resource leak was observed. Natural occurrence and transport delivery timing were not measured.

The dedicated inspect-dart-aot CLI calls the shared service directly and does not use the application wrapper; this report does not claim the same CLI failure.

REA version

rea-agents 6.7.0; source revision 5241597594.

Runtime and platform

macOS 27.0.1 arm64; Node.js v24.18.0 (repository pin), locked Vitest 5.0.3. The initial nine-case run also reproduced on supported Node.js v26.12.0. The final combined focused run passed ten characterization checks on Node v24.18.0.

Target and artifact context

No APK is opened in these cancellation controls. The selected absolute placeholder path is only an accepted request input.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Observed diagnostics from the focused run:

FLUTTER {"reason":null,"serviceExact":true,"applicationExact":false,"providerExact":false}
FLUTTER {"reason":"caller stop","serviceExact":true,"applicationExact":false,"providerExact":false}
FLUTTER {"reason":{"phase":"admission"},"serviceExact":true,"applicationExact":false,"providerExact":false}
FLUTTER_ADMISSION {"reached":true,"sawAborted":true,"tag":"AnalysisCancelledError","exactReason":false}

The tests assert exact reason identity and the reported typed result. They do not assert that an already-cancelled MCP client receives a response.

Typed outcome or recovery expected

An AnalysisCancelledError for inspect_dart_aot whose cause is the exact observed caller reason. Keep pre-admission cancellation free of target acquisition.

Analysis or suspected cause

The application guard and provider guard construct cancellation without options. The MCP registration delegates inspect_dart_aot to the application wrapper. The dedicated CLI instead delegates directly to the shared service. Outer admission can handle an initially aborted MCP call itself; the included session control demonstrates the gap after successful admission.

Additional context

Related reports address different stages: #4640/PR #4762 concern interruption during CLI cleanup retry; #4717/PR #4766 retain completed observations during successful first snapshot cleanup. Their repairs do not change these early guards. #5944 concerns the separate derived-operation helper, #4638 concerns owned commands, and closed PR #4711 concerns Kuna. The relevant source is unchanged on checked upstream main d8a9673fc4. That newer revision was not executed.

Other cancellation-reason reports cover separate producer boundaries: #5935, #5944, #5959, #5960, #5964, #5985, #5987, #5998, #5999, #6002, #6032, #6037, #6042, #6100, #6101. These are related provenance defects; this report concerns the two Flutter early guards.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6110 ### 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 MCP or agent integration ### Observed behavior The production `inspectDartAot` application wrapper returns `AnalysisCancelledError` with `cause === undefined` for an already-observed abort. The exact same signal passed directly to `FlutterBuildAnalysisService.execute` retains its reason. The provider's own early guard also drops it. Null, a primitive string and an object reason all reproduced the loss. A controlled queued abort after real session admission succeeded reached the wrapper with the signal aborted and reproduced the same missing cause. This is an application/session-boundary reproduction, not an SDK or stdio cancellation test. ### Reproduction Use a prepared checkout with the source revision below and locked dependencies. Save the following as `src/application/cancellationReason.repro.test.ts`, then run only that file with the repository-pinned Node.js 24.18.0: ```sh node node_modules/vitest/vitest.mjs run src/application/cancellationReason.repro.test.ts --maxWorkers 1 ``` The assertions characterize the current behavior, so a passing test demonstrates the defect and its controls. Remove the test after running it. ```ts import { writeFile, mkdtemp, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { expect, it } from 'vitest'; import { inspectDartAot } from './flutter/DartAotInspection.js'; import { FlutterBuildAnalysisService } from './flutter/FlutterBuildAnalysisService.js'; import { FlutterBuildProvider } from '../flutter/FlutterBuildProvider.js'; import { inspectAnalysisViewWithFiles } from './analysisView/AnalysisViewService.js'; import { readEvidenceFileSource } from './analysisView/evidenceFileSource.js'; import { BinarySession } from './binary/BinarySession.js'; import { InvestigationRecords } from './investigation/InvestigationRecords.js'; import { createTestProviderRouter } from '../../tests/fixtures/binarySession.js'; import { createAnalysisExecution, type AnalysisClient } from './AnalysisProvider.js'; import { ProviderAdapterError } from '../domain/providerAdapterError.js'; import { createAnalysisProfile } from '../domain/analysisProfile.js'; import { elf } from '../domain/binaryTarget.fixture.js'; import { ok, err } from '../domain/result.js'; it.each([null, 'caller stop', { phase: 'admission' }])('Flutter early cancellation loses exact reason %j', async reason => { const controller = new AbortController(); controller.abort(reason); const provider = new FlutterBuildProvider(); const service = new FlutterBuildAnalysisService(provider); const input = { path: '/targets/unopened.apk' }; const options = { signal: controller.signal }; const control = await service.execute('inspect_dart_aot', input, options); const app = await inspectDartAot(service, input, {}, options); const direct = await provider.execute({ operation: 'inspect_dart_aot', input }, options); expect(control.ok).toBe(false); expect(app.ok).toBe(false); expect(direct.ok).toBe(false); if (control.ok || app.ok || direct.ok) throw new Error('Expected cancellation'); expect(control.error.cause).toBe(reason); expect(app.error._tag).toBe('AnalysisCancelledError'); expect(app.error.cause).toBeUndefined(); expect(direct.error.cause).toBeUndefined(); console.log('FLUTTER', JSON.stringify({ reason, serviceExact: control.error.cause === reason, applicationExact: app.error.cause === reason, providerExact: direct.error.cause === reason })); await provider.close(); }); it('Flutter wrapper can observe abort after real session admission succeeds', async () => { const session = new BinarySession(createTestProviderRouter(() => { throw new Error('must not acquire'); })); const provider = new FlutterBuildProvider(); const service = new FlutterBuildAnalysisService(provider); const controller = new AbortController(); const reason = { phase: 'after-admission' }; let reached = false; let sawAborted = false; const running = session.withAdmittedAnalysis('inspect_dart_aot', controller.signal, async () => { reached = true; sawAborted = controller.signal.aborted; return inspectDartAot(service, { path: '/targets/unopened.apk' }, {}, { signal: controller.signal }); }); queueMicrotask(() => controller.abort(reason)); try { const admitted = await running; expect(admitted.ok).toBe(true); if (!admitted.ok) throw admitted.error; expect(reached).toBe(true); expect(sawAborted).toBe(true); expect(admitted.value.ok).toBe(false); if (admitted.value.ok) throw new Error('Expected cancellation'); expect(admitted.value.error._tag).toBe('AnalysisCancelledError'); expect(admitted.value.error.cause).toBeUndefined(); console.log('FLUTTER_ADMISSION', JSON.stringify({ reached, sawAborted, tag: admitted.value.error._tag, exactReason: admitted.value.error.cause === reason })); } finally { await session.close(); await provider.close(); } }); ``` ### Expected behavior Keep `AnalysisCancelledError.cause === signal.reason` exactly, including null, primitives and object identity. The [lifecycle contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/tool-design.md#L101-L110) requires this, and the shared service already does so at [its admission boundary](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/ProviderOperationExecution.ts#L55-L60). ### Impact and scope P2: the affected typed-result boundary loses required cancellation provenance; successful analysis and the cancellation category are unchanged. Code retaining the original signal can still read the reason there. No changed MCP wire field, input corruption or persistent resource leak was observed. Natural occurrence and transport delivery timing were not measured. The dedicated `inspect-dart-aot` CLI calls the shared service directly and does not use the application wrapper; this report does not claim the same CLI failure. ### REA version rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e. ### Runtime and platform macOS 27.0.1 arm64; Node.js v24.18.0 (repository pin), locked Vitest 5.0.3. The initial nine-case run also reproduced on supported Node.js v26.12.0. The final combined focused run passed ten characterization checks on Node v24.18.0. ### Target and artifact context No APK is opened in these cancellation controls. The selected absolute placeholder path is only an accepted request input. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence Observed diagnostics from the focused run: ```text FLUTTER {"reason":null,"serviceExact":true,"applicationExact":false,"providerExact":false} FLUTTER {"reason":"caller stop","serviceExact":true,"applicationExact":false,"providerExact":false} FLUTTER {"reason":{"phase":"admission"},"serviceExact":true,"applicationExact":false,"providerExact":false} FLUTTER_ADMISSION {"reached":true,"sawAborted":true,"tag":"AnalysisCancelledError","exactReason":false} ``` The tests assert exact reason identity and the reported typed result. They do not assert that an already-cancelled MCP client receives a response. ### Typed outcome or recovery expected An AnalysisCancelledError for inspect_dart_aot whose cause is the exact observed caller reason. Keep pre-admission cancellation free of target acquisition. ### Analysis or suspected cause The [application guard](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/flutter/DartAotInspection.ts#L21-L24) and [provider guard](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/flutter/FlutterBuildProvider.ts#L591-L597) construct cancellation without options. The [MCP registration](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/server/registerFlutterTools.ts#L37-L51) delegates inspect_dart_aot to the application wrapper. The [dedicated CLI](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cli/flutterCommands.ts#L74-L88) instead delegates directly to the shared service. Outer admission can handle an initially aborted MCP call itself; the included session control demonstrates the gap after successful admission. ### Additional context Related reports address different stages: #4640/PR #4762 concern interruption during CLI cleanup retry; #4717/PR #4766 retain completed observations during successful first snapshot cleanup. Their repairs do not change these early guards. #5944 concerns the separate derived-operation helper, #4638 concerns owned commands, and closed PR #4711 concerns Kuna. The relevant source is unchanged on checked upstream main d8a9673fc44fde4bd30ff98c241886e6683580ca. That newer revision was not executed. Other cancellation-reason reports cover separate producer boundaries: [#5935](https://github.com/morluto/rea/issues/5935), [#5944](https://github.com/morluto/rea/issues/5944), [#5959](https://github.com/morluto/rea/issues/5959), [#5960](https://github.com/morluto/rea/issues/5960), [#5964](https://github.com/morluto/rea/issues/5964), [#5985](https://github.com/morluto/rea/issues/5985), [#5987](https://github.com/morluto/rea/issues/5987), [#5998](https://github.com/morluto/rea/issues/5998), [#5999](https://github.com/morluto/rea/issues/5999), [#6002](https://github.com/morluto/rea/issues/6002), [#6032](https://github.com/morluto/rea/issues/6032), [#6037](https://github.com/morluto/rea/issues/6037), [#6042](https://github.com/morluto/rea/issues/6042), [#6100](https://github.com/morluto/rea/issues/6100), [#6101](https://github.com/morluto/rea/issues/6101). These are related provenance defects; this report concerns the two Flutter early guards.
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#3742
No description provided.