[GH-ISSUE #8841] [Bug] push-adb-file rejects successful path probes when adb starts its server #7771

Closed
opened 2026-11-04 00:49:10 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Nov 4, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8841

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

The public push-adb-file CLI stops before transfer with unreadable_output and destination_directory: unknown when its directory test completes successfully with only adb's known server-startup notices on stderr. The CLI exits 1 although the retained probe exit code is 0. Its error says retryable: false and advises that repeating the unchanged request is not expected to succeed; the same request succeeds after the fixture's server has started.

This was observed through the unmodified CLI using a subprocess fixture selected by the documented absolute REA_ADB_PATH. No live device or real adb binary was used. Pinned Android source independently establishes that a successful cold-server shell invocation can produce these exact stderr notices and continue to the remote test. Supported reachability and provider output are source-proved; the resulting CLI refusal is fixture-observed.

Reproduction

Use a built checkout at the revision below, with Node 24.22.1 on PATH. Save this as reproduce.py outside the checkout, then run it from the checkout root:

REPRO_ROOT=$(mktemp -d) python3 /absolute/path/to/reproduce.py

The fixture keeps its HOME, configuration directories, local device stand-in, inputs and transcripts in the generated directory. It invokes the real public CLI, delegates path predicates to /bin/test, copies the actual staged input on successful push, and computes a digest from the copied bytes. The declared adb version is fixture data.

import os, subprocess, pathlib, json, tempfile
FIXTURE = r'''#!/usr/bin/env node
import {appendFileSync,existsSync,writeFileSync,readFileSync,copyFileSync,mkdirSync} from 'node:fs';
import {spawnSync} from 'node:child_process';
import {dirname,basename} from 'node:path';
import {createHash} from 'node:crypto';
const root=process.env.FIXTURE_ROOT;
const mode=process.env.FIXTURE_MODE;
let args=process.argv.slice(2);
appendFileSync(root+'/calls.jsonl',JSON.stringify(args)+'\n');
if(args[0]==='version'){console.log('Android Debug Bridge version 1.0.41\nVersion 37.0.0-14973128'); process.exit(0);}
if(args[0]==='-s') args=args.slice(2);
const mapped=path=>root+'/device'+path;
if(args[0]==='shell' && args[1]==='test'){
  const pred=args[2];
  const shouldNotice=(mode==='cold' && !existsSync(root+'/started'))||(mode==='exist-cold' && pred==='-e');
  if(shouldNotice){process.stderr.write('* daemon not running; starting now at tcp:5037\n* daemon started successfully\n');writeFileSync(root+'/started','');}
  if(mode==='unexpected')process.stderr.write('unexpected diagnostic\n');
  const r=spawnSync('/bin/test',[pred,mapped(args[3])]);
  process.exit(r.status??2);
}
if(args[0]==='push'){
  let dest=mapped(args[2]);
  if(spawnSync('/bin/test',['-d',dest]).status===0)dest+='/'+basename(args[1]);
  mkdirSync(dirname(dest),{recursive:true});copyFileSync(args[1],dest);console.log('1 file pushed');process.exit(0);
}
if(args[0]==='shell' && args[1]==='sha256sum'){
 console.log(createHash('sha256').update(readFileSync(mapped(args[2]))).digest('hex')+'  '+args[2]);process.exit(0);
}
console.error('fixture unsupported invocation',args);process.exit(2);
'''
repo = pathlib.Path.cwd()
root = pathlib.Path(os.environ.get('REPRO_ROOT') or tempfile.mkdtemp(prefix='rea-probe-')).resolve()
root.mkdir(parents=True, exist_ok=True)
for name in ['home', 'runtime']:
    (root / name).mkdir(exist_ok=True)
(root / 'input.txt').write_text('ordinary verification input\n')
adb = root / 'adb'
adb.write_text(FIXTURE)
adb.chmod(0o755)
entry = os.environ.get('REA_ENTRY', str(repo / 'scripts/rea.mjs'))
for case, mode, destination in [('cold-directory', 'cold', '/sdcard/rea-probe'), ('warm-retry', 'cold', '/sdcard/rea-probe'), ('clean', 'warm', '/sdcard/rea-probe'), ('unexpected', 'unexpected', '/sdcard/rea-probe'), ('cold-file', 'cold', '/sdcard/rea-probe/input.txt'), ('exist-cold', 'exist-cold', '/sdcard/rea-probe/input.txt')]:
    work = root / ('cold-directory' if case == 'warm-retry' else case)
    (work / 'device/sdcard/rea-probe').mkdir(parents=True, exist_ok=True)
    env = {'PATH': os.environ['PATH'], 'HOME': str(root/'home'), 'TMPDIR': str(root/'runtime'), 'XDG_CONFIG_HOME': str(root/'home/.config'), 'XDG_CACHE_HOME': str(root/'home/.cache'), 'REA_ADB_PATH': str(adb), 'FIXTURE_ROOT': str(work), 'FIXTURE_MODE': mode}
    command = ['node', entry, 'push-adb-file', 'emulator-5554', str(root/'input.txt'), destination, '--format', 'json']
    run = subprocess.run(command, cwd=repo, env=env, capture_output=True, text=True, timeout=60)
    result = json.loads(run.stdout)
    calls = [json.loads(line) for line in (work/'calls.jsonl').read_text().splitlines()]
    data = {'case': case, 'exit': run.returncode, 'code': result.get('code'), 'diagnostics': result.get('details', {}).get('diagnostics'), 'captured_output': result.get('details', {}).get('captured_output'), 'transferred': (work/'device/sdcard/rea-probe/input.txt').exists()}
    (root/(case+'.json')).write_text(json.dumps({'command':command, 'stdout':run.stdout, 'stderr':run.stderr, 'calls':calls, 'summary':data}, indent=2))
    print(json.dumps(data))

I ran this six-case matrix against the built source checkout and independently installed npm release rea-agents@6.8.0. Both returned the same results. The cold-directory case fails; warm-retry uses the same fixture state and succeeds; clean succeeds; unexpected remains refused. cold-file demonstrates the same problem with a legitimate false directory test. exist-cold injects startup at the later existence probe to check the shared parser; it is not the ordinary first-use sequence.

Expected behavior

A completed directory test should resolve its boolean result when stderr contains only recognized host-side successful server-startup notices. An ordinary first push to an existing directory should not require callers to start adb separately. Keep the original captured output available and preserve refusal for unrecognized diagnostics, failed startup, cancellation, truncation and incomplete execution. The existing overwrite safety check should remain in place.

Impact and scope

P2: a bounded cold-server push fails before transfer and provides misleading nonretryable guidance. The warm-state retry and clean-server controls succeeded on both tested runtimes, providing a practical workaround. In the fixture, no destination bytes were written by the failed requests. No real-device frequency, USB startup timing, Windows/Linux runtime behavior or data loss was established.

The directory probe is the first server-contacting command in this workflow; preceding adb version checks do not start a server. The existence probe uses the same interpretation and also rejects injected known notices, but ordinary initial startup happens at the earlier directory probe. MCP shares the provider by source inspection; MCP execution was not tested.

REA version

Source c3647ade1e557c4179a8b825a37e0c2193cd2526, package and CLI version 6.8.0. Independently installed npm rea-agents@6.8.0 also reports 6.8.0 and shows the same behavior. npm view rea-agents version returned 6.8.0. Fetched origin/main resolved to the same source revision.

Runtime and platform

Node v24.22.1; macOS 27.2, Darwin arm64. The adb executable was an inert local fixture; its reported Android client version is synthetic.

Target and artifact context

Synthetic serial emulator-5554, an existing directory /sdcard/rea-probe, no preexisting input.txt child, and a 28-byte ordinary local input. Device behavior is represented by local files; no Android device, emulator or network service is contacted.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Both runtimes produced these meaningful results:

{"case":"cold-directory","exit":1,"code":"unreadable_output","diagnostics":{"destination_directory":"unknown"},"captured_output":{"stdout":"","stderr":"* daemon not running; starting now at tcp:5037\n* daemon started successfully\n","stdout_bytes":0,"stderr_bytes":77,"exit_code":0,"signal":null,"truncated":false},"transferred":false}
{"case":"warm-retry","exit":0,"code":null,"diagnostics":null,"captured_output":null,"transferred":true}
{"case":"clean","exit":0,"code":null,"diagnostics":null,"captured_output":null,"transferred":true}
{"case":"unexpected","exit":1,"code":"unreadable_output","diagnostics":{"destination_directory":"unknown"},"captured_output":{"stdout":"","stderr":"unexpected diagnostic\n","stdout_bytes":0,"stderr_bytes":22,"exit_code":0,"signal":null,"truncated":false},"transferred":false}
{"case":"cold-file","exit":1,"code":"unreadable_output","diagnostics":{"destination_directory":"unknown"},"captured_output":{"stdout":"","stderr":"* daemon not running; starting now at tcp:5037\n* daemon started successfully\n","stdout_bytes":0,"stderr_bytes":77,"exit_code":1,"signal":null,"truncated":false},"transferred":false}
{"case":"exist-cold","exit":1,"code":"unreadable_output","diagnostics":{"destination_existence":"unknown"},"captured_output":{"stdout":"","stderr":"* daemon not running; starting now at tcp:5037\n* daemon started successfully\n","stdout_bytes":0,"stderr_bytes":77,"exit_code":1,"signal":null,"truncated":false},"transferred":false}

The first cold request's recorded argv reaches -s emulator-5554 shell test -d /sdcard/rea-probe after client-version checks and never reaches push. Successful controls return matching host/device SHA-256 for the 28 copied bytes. The unrelated diagnostic is still rejected.

Expected source: REA documents first-query server startup and existing-directory destinations. Client inspection does not start the server. The provider invokes the directory probe before transfer.

Independent Android source: the shell command queries server/device features; successful cold startup prints both notices to stderr and continues; shell-v2 returns the remote exit status. These are source-derived facts, not claims of a live-provider run.

npm run test:focused -- src/adb/AdbCommand.test.ts passed all 21 existing tests. Those tests do not cover successful startup notices in a path-probe result. No full suite was run.

Typed outcome or recovery expected

For the completed exit-zero directory test with only recognized startup notices, continue with the effective child path, enforce the normal existence/overwrite check, transfer once and return the normal receipt. A successful completed exit-one test with only those host notices should retain its false predicate result. Other diagnostics and incomplete results should keep destination status unknown and stop before push.

Analysis or suspected cause

Source-derived: probeAdbPath requires both streams to be empty, so valid informational stderr makes a completed probe unreadable. The failure classifier already recognizes startup notices before interpreting actual diagnostics, and the device-list parser recognizes successful startup. That known host protocol is not interpreted by the path-probe owner.

Additional context

Searched open and closed issues and PRs by command names, daemon notices, path-probe diagnostics and test predicates; no current open exact duplicate was found. Closed #3051 and superseded PR #3073 concern the device-list server-startup flag. Closed #4574 concerns proceeding after genuinely ambiguous existence failures; its fail-closed correction must be preserved. Closed #5051 concerns classifying failed commands before display truncation. None resolves successful path probes with recognized startup notices. Last known good version: NOT_ENOUGH_INFO.

Originally created by @coygeek on GitHub (Nov 4, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8841 ### 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 The public `push-adb-file` CLI stops before transfer with `unreadable_output` and `destination_directory: unknown` when its directory test completes successfully with only adb's known server-startup notices on stderr. The CLI exits 1 although the retained probe exit code is 0. Its error says `retryable: false` and advises that repeating the unchanged request is not expected to succeed; the same request succeeds after the fixture's server has started. This was observed through the unmodified CLI using a subprocess fixture selected by the documented absolute `REA_ADB_PATH`. No live device or real adb binary was used. Pinned Android source independently establishes that a successful cold-server shell invocation can produce these exact stderr notices and continue to the remote test. Supported reachability and provider output are source-proved; the resulting CLI refusal is fixture-observed. ### Reproduction Use a built checkout at the revision below, with Node 24.22.1 on PATH. Save this as `reproduce.py` outside the checkout, then run it from the checkout root: ```sh REPRO_ROOT=$(mktemp -d) python3 /absolute/path/to/reproduce.py ``` The fixture keeps its HOME, configuration directories, local device stand-in, inputs and transcripts in the generated directory. It invokes the real public CLI, delegates path predicates to `/bin/test`, copies the actual staged input on successful push, and computes a digest from the copied bytes. The declared adb version is fixture data. ```python import os, subprocess, pathlib, json, tempfile FIXTURE = r'''#!/usr/bin/env node import {appendFileSync,existsSync,writeFileSync,readFileSync,copyFileSync,mkdirSync} from 'node:fs'; import {spawnSync} from 'node:child_process'; import {dirname,basename} from 'node:path'; import {createHash} from 'node:crypto'; const root=process.env.FIXTURE_ROOT; const mode=process.env.FIXTURE_MODE; let args=process.argv.slice(2); appendFileSync(root+'/calls.jsonl',JSON.stringify(args)+'\n'); if(args[0]==='version'){console.log('Android Debug Bridge version 1.0.41\nVersion 37.0.0-14973128'); process.exit(0);} if(args[0]==='-s') args=args.slice(2); const mapped=path=>root+'/device'+path; if(args[0]==='shell' && args[1]==='test'){ const pred=args[2]; const shouldNotice=(mode==='cold' && !existsSync(root+'/started'))||(mode==='exist-cold' && pred==='-e'); if(shouldNotice){process.stderr.write('* daemon not running; starting now at tcp:5037\n* daemon started successfully\n');writeFileSync(root+'/started','');} if(mode==='unexpected')process.stderr.write('unexpected diagnostic\n'); const r=spawnSync('/bin/test',[pred,mapped(args[3])]); process.exit(r.status??2); } if(args[0]==='push'){ let dest=mapped(args[2]); if(spawnSync('/bin/test',['-d',dest]).status===0)dest+='/'+basename(args[1]); mkdirSync(dirname(dest),{recursive:true});copyFileSync(args[1],dest);console.log('1 file pushed');process.exit(0); } if(args[0]==='shell' && args[1]==='sha256sum'){ console.log(createHash('sha256').update(readFileSync(mapped(args[2]))).digest('hex')+' '+args[2]);process.exit(0); } console.error('fixture unsupported invocation',args);process.exit(2); ''' repo = pathlib.Path.cwd() root = pathlib.Path(os.environ.get('REPRO_ROOT') or tempfile.mkdtemp(prefix='rea-probe-')).resolve() root.mkdir(parents=True, exist_ok=True) for name in ['home', 'runtime']: (root / name).mkdir(exist_ok=True) (root / 'input.txt').write_text('ordinary verification input\n') adb = root / 'adb' adb.write_text(FIXTURE) adb.chmod(0o755) entry = os.environ.get('REA_ENTRY', str(repo / 'scripts/rea.mjs')) for case, mode, destination in [('cold-directory', 'cold', '/sdcard/rea-probe'), ('warm-retry', 'cold', '/sdcard/rea-probe'), ('clean', 'warm', '/sdcard/rea-probe'), ('unexpected', 'unexpected', '/sdcard/rea-probe'), ('cold-file', 'cold', '/sdcard/rea-probe/input.txt'), ('exist-cold', 'exist-cold', '/sdcard/rea-probe/input.txt')]: work = root / ('cold-directory' if case == 'warm-retry' else case) (work / 'device/sdcard/rea-probe').mkdir(parents=True, exist_ok=True) env = {'PATH': os.environ['PATH'], 'HOME': str(root/'home'), 'TMPDIR': str(root/'runtime'), 'XDG_CONFIG_HOME': str(root/'home/.config'), 'XDG_CACHE_HOME': str(root/'home/.cache'), 'REA_ADB_PATH': str(adb), 'FIXTURE_ROOT': str(work), 'FIXTURE_MODE': mode} command = ['node', entry, 'push-adb-file', 'emulator-5554', str(root/'input.txt'), destination, '--format', 'json'] run = subprocess.run(command, cwd=repo, env=env, capture_output=True, text=True, timeout=60) result = json.loads(run.stdout) calls = [json.loads(line) for line in (work/'calls.jsonl').read_text().splitlines()] data = {'case': case, 'exit': run.returncode, 'code': result.get('code'), 'diagnostics': result.get('details', {}).get('diagnostics'), 'captured_output': result.get('details', {}).get('captured_output'), 'transferred': (work/'device/sdcard/rea-probe/input.txt').exists()} (root/(case+'.json')).write_text(json.dumps({'command':command, 'stdout':run.stdout, 'stderr':run.stderr, 'calls':calls, 'summary':data}, indent=2)) print(json.dumps(data)) ``` I ran this six-case matrix against the built source checkout and independently installed npm release `rea-agents@6.8.0`. Both returned the same results. The `cold-directory` case fails; `warm-retry` uses the same fixture state and succeeds; `clean` succeeds; `unexpected` remains refused. `cold-file` demonstrates the same problem with a legitimate false directory test. `exist-cold` injects startup at the later existence probe to check the shared parser; it is not the ordinary first-use sequence. ### Expected behavior A completed directory test should resolve its boolean result when stderr contains only recognized host-side successful server-startup notices. An ordinary first push to an existing directory should not require callers to start adb separately. Keep the original captured output available and preserve refusal for unrecognized diagnostics, failed startup, cancellation, truncation and incomplete execution. The existing overwrite safety check should remain in place. ### Impact and scope P2: a bounded cold-server push fails before transfer and provides misleading nonretryable guidance. The warm-state retry and clean-server controls succeeded on both tested runtimes, providing a practical workaround. In the fixture, no destination bytes were written by the failed requests. No real-device frequency, USB startup timing, Windows/Linux runtime behavior or data loss was established. The directory probe is the first server-contacting command in this workflow; preceding `adb version` checks do not start a server. The existence probe uses the same interpretation and also rejects injected known notices, but ordinary initial startup happens at the earlier directory probe. MCP shares the provider by source inspection; MCP execution was not tested. ### REA version Source `c3647ade1e557c4179a8b825a37e0c2193cd2526`, package and CLI version 6.8.0. Independently installed npm `rea-agents@6.8.0` also reports 6.8.0 and shows the same behavior. `npm view rea-agents version` returned 6.8.0. Fetched `origin/main` resolved to the same source revision. ### Runtime and platform Node v24.22.1; macOS 27.2, Darwin arm64. The adb executable was an inert local fixture; its reported Android client version is synthetic. ### Target and artifact context Synthetic serial `emulator-5554`, an existing directory `/sdcard/rea-probe`, no preexisting `input.txt` child, and a 28-byte ordinary local input. Device behavior is represented by local files; no Android device, emulator or network service is contacted. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Both runtimes produced these meaningful results: ```jsonl {"case":"cold-directory","exit":1,"code":"unreadable_output","diagnostics":{"destination_directory":"unknown"},"captured_output":{"stdout":"","stderr":"* daemon not running; starting now at tcp:5037\n* daemon started successfully\n","stdout_bytes":0,"stderr_bytes":77,"exit_code":0,"signal":null,"truncated":false},"transferred":false} {"case":"warm-retry","exit":0,"code":null,"diagnostics":null,"captured_output":null,"transferred":true} {"case":"clean","exit":0,"code":null,"diagnostics":null,"captured_output":null,"transferred":true} {"case":"unexpected","exit":1,"code":"unreadable_output","diagnostics":{"destination_directory":"unknown"},"captured_output":{"stdout":"","stderr":"unexpected diagnostic\n","stdout_bytes":0,"stderr_bytes":22,"exit_code":0,"signal":null,"truncated":false},"transferred":false} {"case":"cold-file","exit":1,"code":"unreadable_output","diagnostics":{"destination_directory":"unknown"},"captured_output":{"stdout":"","stderr":"* daemon not running; starting now at tcp:5037\n* daemon started successfully\n","stdout_bytes":0,"stderr_bytes":77,"exit_code":1,"signal":null,"truncated":false},"transferred":false} {"case":"exist-cold","exit":1,"code":"unreadable_output","diagnostics":{"destination_existence":"unknown"},"captured_output":{"stdout":"","stderr":"* daemon not running; starting now at tcp:5037\n* daemon started successfully\n","stdout_bytes":0,"stderr_bytes":77,"exit_code":1,"signal":null,"truncated":false},"transferred":false} ``` The first cold request's recorded argv reaches `-s emulator-5554 shell test -d /sdcard/rea-probe` after client-version checks and never reaches `push`. Successful controls return matching host/device SHA-256 for the 28 copied bytes. The unrelated diagnostic is still rejected. Expected source: [REA documents first-query server startup](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/docs/adb-device-analysis.md#L10-L13) and [existing-directory destinations](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/docs/adb-device-analysis.md#L110-L113). [Client inspection does not start the server](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/adb/AdbConfiguration.ts#L167-L170). The [provider invokes the directory probe before transfer](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/adb/AdbProvider.ts#L2362-L2449). Independent Android source: [the shell command queries server/device features](https://android.googlesource.com/platform/packages/modules/adb/+/ec7028fb5c2cde5c18522dd08f7c34c5e668fc83/client/commandline.cpp#688); [successful cold startup prints both notices to stderr and continues](https://android.googlesource.com/platform/packages/modules/adb/+/ec7028fb5c2cde5c18522dd08f7c34c5e668fc83/client/adb_client.cpp#258); [shell-v2 returns the remote exit status](https://android.googlesource.com/platform/packages/modules/adb/+/ec7028fb5c2cde5c18522dd08f7c34c5e668fc83/client/commandline.cpp#290). These are source-derived facts, not claims of a live-provider run. `npm run test:focused -- src/adb/AdbCommand.test.ts` passed all 21 existing tests. Those tests do not cover successful startup notices in a path-probe result. No full suite was run. ### Typed outcome or recovery expected For the completed exit-zero directory test with only recognized startup notices, continue with the effective child path, enforce the normal existence/overwrite check, transfer once and return the normal receipt. A successful completed exit-one test with only those host notices should retain its false predicate result. Other diagnostics and incomplete results should keep destination status unknown and stop before push. ### Analysis or suspected cause Source-derived: [`probeAdbPath`](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/adb/AdbCommand.ts#L298-L368) requires both streams to be empty, so valid informational stderr makes a completed probe unreadable. The [failure classifier](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/adb/AdbCommand.ts#L150-L170) already recognizes startup notices before interpreting actual diagnostics, and the [device-list parser](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/adb/AdbDeviceOutput.ts#L36-L40) recognizes successful startup. That known host protocol is not interpreted by the path-probe owner. ### Additional context Searched open and closed issues and PRs by command names, daemon notices, path-probe diagnostics and `test` predicates; no current open exact duplicate was found. Closed [#3051](https://github.com/morluto/rea/issues/3051) and superseded [PR #3073](https://github.com/morluto/rea/pull/3073) concern the device-list server-startup flag. Closed [#4574](https://github.com/morluto/rea/issues/4574) concerns proceeding after genuinely ambiguous existence failures; its fail-closed correction must be preserved. Closed [#5051](https://github.com/morluto/rea/issues/5051) concerns classifying failed commands before display truncation. None resolves successful path probes with recognized startup notices. Last known good version: NOT_ENOUGH_INFO.
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#7771
No description provided.