[GH-ISSUE #4608] [Bug] inspect-adb-window returns absent focus as the string null #2394

Closed
opened 2026-10-17 21:04:05 +00:00 by bsakaguchi · 2 comments
Owner

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

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 a caller-selected fake adb emits mCurrentFocus=null and mFocusedApp=null, inspect-adb-window SERIAL --json returns focused_window: "null" and focused_app: "null". Both tested builds produced these non-null strings through the windows section and the displays fallback. All calls exited 0 with empty stderr. This is an executed CLI reproduction with simulated adb output; no physical device was used.

Reproduction

With the CLI available, run the following from the source checkout or unpacked npm package root. The source build used npm ci and npm run build:cached with the pinned development toolchain. The npm tarball used npm install --ignore-scripts to install dependencies. The script selects fake adb through REA_ADB_PATH and gives each CLI call a fresh HOME and an environment containing only HOME, PATH, and REA_ADB_PATH.

python3 - scripts/rea.mjs <<'PY'
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile

node = subprocess.check_output(["node", "-p", "process.execPath"], text=True).strip()
cli = str(Path(sys.argv[1]).resolve())
fixture = r'''#!/bin/sh
if [ "$1" = version ]; then
  printf 'Android Debug Bridge version 1.0.41\nVersion 34.0.5-fixture\nInstalled as %s\n' "$0"
  exit 0
fi
if [ "$1" = -s ] && [ "$3" = shell ]; then
  case "$4:$5:$6" in
    dumpsys:window:windows|dumpsys:window:displays)
      if [ "$6" = windows ]; then
        printf 'WINDOW MANAGER WINDOWS (dumpsys window windows)\n'
        case "$2" in fallback-*) exit 0;; esac
      else
        printf 'WINDOW MANAGER DISPLAY CONTENTS (dumpsys window displays)\n  Display: mDisplayId=0\n'
      fi
      case "$2" in
        focused-device) printf '  mCurrentFocus=Window{abcdef u0 com.example/.MainActivity}\n  mFocusedApp=ActivityRecord{123abc u0 com.example/.MainActivity t1}\n';;
        missing-device) :;;
        *) printf '  mCurrentFocus=null\n  mFocusedApp=null\n';;
      esac
      exit 0;;
  esac
fi
exit 1
'''
with tempfile.TemporaryDirectory() as root:
    root = Path(root)
    adb = root / "adb"
    adb.write_text(fixture)
    adb.chmod(0o755)
    serials = ["null-device", "null-device", "fallback-null-device", "fallback-null-device", "focused-device", "missing-device"]
    for index, serial in enumerate(serials):
        home = root / f"home-{index}"
        home.mkdir()
        env = {"HOME": str(home), "PATH": str(Path(node).parent) + os.pathsep + os.defpath, "REA_ADB_PATH": str(adb)}
        result = subprocess.run([node, cli, "inspect-adb-window", serial, "--json"], env=env, text=True, capture_output=True)
        assert result.returncode == 0, result.stdout + result.stderr
        assert not result.stderr, result.stderr
        evidence = json.loads(result.stdout)
        answer = evidence["normalized_result"]
        answer.pop("client")
        print(json.dumps({"serial": serial, "exit": result.returncode, "answer": answer, "limitations": evidence["limitations"]}))
        fields = ("focused_window", "focused_app")
        corrected = {key: None if answer[key] == "null" else answer[key] for key in fields}
        if serial == "focused-device":
            assert all(corrected[key] == answer[key] and corrected[key] is not None for key in fields)
        else:
            assert all(corrected[key] is None for key in fields)
PY

For each build, explicit null reproduced 2/2 through windows and 2/2 through displays. The named-window and missing-declaration controls each ran once per build. The script also executed the caller-side conversion described below against every result.

Expected behavior

The normalized focus fields should represent an explicitly absent focused object as JSON null, while retaining evidence that distinguishes explicit absence from an omitted declaration. Actual Window and ActivityRecord identities should remain strings. The tool promises current window-manager focus facts, and AGENTS.md requires interpretations to preserve identity, omission, uncertainty, and source evidence across boundaries. The nullable-string schema accepts both representations, so this concerns the meaning of normalized facts rather than schema validity.

Impact and scope

Callers that check whether the focus fields are non-null receive the wrong presence result for the explicit-null input. The observed scope is the CLI on both tested builds and both source-section routes. Physical-device frequency and downstream actions were not measured.

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The supported CLI returns a non-null focused-object string when its producer explicitly declares no object. This is a bounded machine-readable behavior defect with a tested caller-side workaround.
  • Workaround status: available
  • Workaround basis: Converting exactly the two focus fields' string "null" values to JSON null preserved all expected focus values in the executed explicit-null, named-object, and missing-declaration cases. Retain the original evidence and limitations to distinguish explicit absence from unknown focus.

REA version

Current-main source commit 8267edc0ef and the npm release rea-agents@6.5.0. Both CLI launchers reported 6.5.0.

Runtime and platform

Node.js v24.18.0; Darwin arm64. The fake client identifies itself as adb 34.0.5-fixture. No platform-tools installation or Android device was involved.

Target and artifact context

Reduced dumpsys output with one pair of focus declarations, either explicit null references or named objects; a separate control omits both declarations in both sections. Multi-display selection and saved ANR sections are outside this report.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

  • Expected source: src/contracts/adb/adbToolContracts.ts:224-230, docs/adb-device-analysis.md:88, and AGENTS.md:19-31.
  • Failure source: Executed CLI results below; parser at src/adb/AdbShellOutput.ts:184-200 and result projection at src/adb/AdbProvider.ts:1738-1783 in the main build.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Both builds returned these focus fields. The excerpts omit unrelated client identity.

null-device, twice per build:
{"focused_window":"null","focused_app":"null","observed_lines":["null","null"]}
limitation: Focus declarations were read from `dumpsys window windows`

fallback-null-device, twice per build:
{"focused_window":"null","focused_app":"null","observed_lines":["null","null"]}
limitation: Focus declarations were read from `dumpsys window displays`

focused-device, once per build:
{"focused_window":"Window{abcdef u0 com.example/.MainActivity}","focused_app":"ActivityRecord{123abc u0 com.example/.MainActivity t1}"}

missing-device, once per build:
{"focused_window":null,"focused_app":null,"observed_lines":[]}
limitation: Neither `dumpsys window windows` nor `dumpsys window displays` declared mCurrentFocus or mFocusedApp; focus is unknown, not absent

Source-derived Android format check: Android's release-tagged DisplayContent source prints both object references through PrintWriter.println in its dump method at lines 3342-3343, which emits literal null when a reference is absent. RootWindowContainer calls that method in dumpDisplayContents at lines 1046-1053, and WindowManagerService routes displays there at lines 6478-6480. This supports the fixture format on the supported fallback path; it does not establish device-state frequency or a physical-device reproduction.

Typed outcome or recovery expected

For explicit-null input, return JSON null in both focused-object fields and preserve that the source explicitly declared absence. Preserve the named-object control and the missing-declaration control's unknown limitation. Explicitly declared absence does not itself require another section read. The change should restore the same meaning through both section routes.

Analysis or suspected cause

Source-derived: the parser stores each trimmed suffix as a string and the provider forwards it into the normalized result. It does not interpret the null object reference. The section-source limitation correctly reports where declarations were read and does not itself assert that a focused object exists. MCP shares this provider path but was not executed.

Additional context

The open and closed tracker search found related but distinct reports. Issue #2996 concerns omitted declarations and the fallback. The discussion on PR #2998 mentions literal null from an inactive display but requests selecting the focused display among multiple blocks. Issue #3943 concerns installer absence in a different parser and explicitly excludes window focus. This report concerns explicit absence with a single pair of focus declarations, independent of section or display selection.

Originally created by @coygeek on GitHub (Oct 16, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4608 ### 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 a caller-selected fake adb emits `mCurrentFocus=null` and `mFocusedApp=null`, `inspect-adb-window SERIAL --json` returns `focused_window: "null"` and `focused_app: "null"`. Both tested builds produced these non-null strings through the `windows` section and the `displays` fallback. All calls exited 0 with empty stderr. This is an executed CLI reproduction with simulated adb output; no physical device was used. ### Reproduction With the CLI available, run the following from the source checkout or unpacked npm package root. The source build used `npm ci` and `npm run build:cached` with the pinned development toolchain. The npm tarball used `npm install --ignore-scripts` to install dependencies. The script selects fake adb through `REA_ADB_PATH` and gives each CLI call a fresh HOME and an environment containing only HOME, PATH, and REA_ADB_PATH. ```sh python3 - scripts/rea.mjs <<'PY' import json import os from pathlib import Path import subprocess import sys import tempfile node = subprocess.check_output(["node", "-p", "process.execPath"], text=True).strip() cli = str(Path(sys.argv[1]).resolve()) fixture = r'''#!/bin/sh if [ "$1" = version ]; then printf 'Android Debug Bridge version 1.0.41\nVersion 34.0.5-fixture\nInstalled as %s\n' "$0" exit 0 fi if [ "$1" = -s ] && [ "$3" = shell ]; then case "$4:$5:$6" in dumpsys:window:windows|dumpsys:window:displays) if [ "$6" = windows ]; then printf 'WINDOW MANAGER WINDOWS (dumpsys window windows)\n' case "$2" in fallback-*) exit 0;; esac else printf 'WINDOW MANAGER DISPLAY CONTENTS (dumpsys window displays)\n Display: mDisplayId=0\n' fi case "$2" in focused-device) printf ' mCurrentFocus=Window{abcdef u0 com.example/.MainActivity}\n mFocusedApp=ActivityRecord{123abc u0 com.example/.MainActivity t1}\n';; missing-device) :;; *) printf ' mCurrentFocus=null\n mFocusedApp=null\n';; esac exit 0;; esac fi exit 1 ''' with tempfile.TemporaryDirectory() as root: root = Path(root) adb = root / "adb" adb.write_text(fixture) adb.chmod(0o755) serials = ["null-device", "null-device", "fallback-null-device", "fallback-null-device", "focused-device", "missing-device"] for index, serial in enumerate(serials): home = root / f"home-{index}" home.mkdir() env = {"HOME": str(home), "PATH": str(Path(node).parent) + os.pathsep + os.defpath, "REA_ADB_PATH": str(adb)} result = subprocess.run([node, cli, "inspect-adb-window", serial, "--json"], env=env, text=True, capture_output=True) assert result.returncode == 0, result.stdout + result.stderr assert not result.stderr, result.stderr evidence = json.loads(result.stdout) answer = evidence["normalized_result"] answer.pop("client") print(json.dumps({"serial": serial, "exit": result.returncode, "answer": answer, "limitations": evidence["limitations"]})) fields = ("focused_window", "focused_app") corrected = {key: None if answer[key] == "null" else answer[key] for key in fields} if serial == "focused-device": assert all(corrected[key] == answer[key] and corrected[key] is not None for key in fields) else: assert all(corrected[key] is None for key in fields) PY ``` For each build, explicit null reproduced 2/2 through `windows` and 2/2 through `displays`. The named-window and missing-declaration controls each ran once per build. The script also executed the caller-side conversion described below against every result. ### Expected behavior The normalized focus fields should represent an explicitly absent focused object as JSON null, while retaining evidence that distinguishes explicit absence from an omitted declaration. Actual Window and ActivityRecord identities should remain strings. The tool promises current window-manager focus facts, and AGENTS.md requires interpretations to preserve identity, omission, uncertainty, and source evidence across boundaries. The nullable-string schema accepts both representations, so this concerns the meaning of normalized facts rather than schema validity. ### Impact and scope Callers that check whether the focus fields are non-null receive the wrong presence result for the explicit-null input. The observed scope is the CLI on both tested builds and both source-section routes. Physical-device frequency and downstream actions were not measured. - Impact level: P2 - Assessment status: supported - Impact basis: The supported CLI returns a non-null focused-object string when its producer explicitly declares no object. This is a bounded machine-readable behavior defect with a tested caller-side workaround. - Workaround status: available - Workaround basis: Converting exactly the two focus fields' string `"null"` values to JSON null preserved all expected focus values in the executed explicit-null, named-object, and missing-declaration cases. Retain the original evidence and limitations to distinguish explicit absence from unknown focus. ### REA version Current-main source commit 8267edc0ef8c49f617e8935df475edc8e496202c and the npm release rea-agents@6.5.0. Both CLI launchers reported 6.5.0. ### Runtime and platform Node.js v24.18.0; Darwin arm64. The fake client identifies itself as adb 34.0.5-fixture. No platform-tools installation or Android device was involved. ### Target and artifact context Reduced dumpsys output with one pair of focus declarations, either explicit null references or named objects; a separate control omits both declarations in both sections. Multi-display selection and saved ANR sections are outside this report. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence - Expected source: `src/contracts/adb/adbToolContracts.ts:224-230`, `docs/adb-device-analysis.md:88`, and `AGENTS.md:19-31`. - Failure source: Executed CLI results below; parser at `src/adb/AdbShellOutput.ts:184-200` and result projection at `src/adb/AdbProvider.ts:1738-1783` in the main build. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete Both builds returned these focus fields. The excerpts omit unrelated client identity. ```text null-device, twice per build: {"focused_window":"null","focused_app":"null","observed_lines":["null","null"]} limitation: Focus declarations were read from `dumpsys window windows` fallback-null-device, twice per build: {"focused_window":"null","focused_app":"null","observed_lines":["null","null"]} limitation: Focus declarations were read from `dumpsys window displays` focused-device, once per build: {"focused_window":"Window{abcdef u0 com.example/.MainActivity}","focused_app":"ActivityRecord{123abc u0 com.example/.MainActivity t1}"} missing-device, once per build: {"focused_window":null,"focused_app":null,"observed_lines":[]} limitation: Neither `dumpsys window windows` nor `dumpsys window displays` declared mCurrentFocus or mFocusedApp; focus is unknown, not absent ``` Source-derived Android format check: Android's release-tagged [DisplayContent source](https://raw.githubusercontent.com/aosp-mirror/platform_frameworks_base/android-14.0.0_r1/services/core/java/com/android/server/wm/DisplayContent.java) prints both object references through `PrintWriter.println` in its dump method at lines 3342-3343, which emits literal null when a reference is absent. [RootWindowContainer](https://raw.githubusercontent.com/aosp-mirror/platform_frameworks_base/android-14.0.0_r1/services/core/java/com/android/server/wm/RootWindowContainer.java) calls that method in `dumpDisplayContents` at lines 1046-1053, and [WindowManagerService](https://raw.githubusercontent.com/aosp-mirror/platform_frameworks_base/android-14.0.0_r1/services/core/java/com/android/server/wm/WindowManagerService.java) routes `displays` there at lines 6478-6480. This supports the fixture format on the supported fallback path; it does not establish device-state frequency or a physical-device reproduction. ### Typed outcome or recovery expected For explicit-null input, return JSON null in both focused-object fields and preserve that the source explicitly declared absence. Preserve the named-object control and the missing-declaration control's unknown limitation. Explicitly declared absence does not itself require another section read. The change should restore the same meaning through both section routes. ### Analysis or suspected cause Source-derived: the parser stores each trimmed suffix as a string and the provider forwards it into the normalized result. It does not interpret the null object reference. The section-source limitation correctly reports where declarations were read and does not itself assert that a focused object exists. MCP shares this provider path but was not executed. ### Additional context The open and closed tracker search found related but distinct reports. [Issue #2996](https://github.com/morluto/rea/issues/2996) concerns omitted declarations and the fallback. [The discussion on PR #2998](https://github.com/morluto/rea/pull/2998#discussion_r4263050910) mentions literal null from an inactive display but requests selecting the focused display among multiple blocks. [Issue #3943](https://github.com/morluto/rea/issues/3943) concerns installer absence in a different parser and explicitly excludes window focus. This report concerns explicit absence with a single pair of focus declarations, independent of section or display selection.
Author
Owner

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

Addressed in 3d028dbc7.

The parser now owns declaration presence, the normalized object-reference value, and its original source line. Explicit null therefore becomes JSON null without being confused with an omitted declaration. The provider bases fallback on declaration presence; explicit absence does not cause another section read. observed_lines now preserves full named declaration lines (including indentation), making missing individual fields distinguishable from explicit absence. Named object identity values remain strings.

This follows the release-tagged Android DisplayContent dump producer, which prints the two references directly. Selection among multiple display blocks remains separate from this fix.

Validation on Node 24.18.0:

  • New parser/provider controls before repair: 4 failed, 76 passed.
  • Final 86 focused tests passed, including public CLI and production MCP with the actual advertised input/output schemas. Controls cover explicit-null windows/displays, named objects, omitted declarations, duplicate declarations, preserved source lines, and exact shell command counts.
  • Docs checks and five README conformance tests passed. Fake ADB only; no physical-device verification or full CI outcome is claimed.

Bounded completed CLI comparison against immediate parent bfb7ed0bd: two alternating warm repeats/build plus separate first processes. Warm medians baseline/candidate: elapsed 0.35554/0.35468 s; CPU 0.54808/0.55076 s; sampled process-family RSS 352894976/350420992 bytes; raw reply cl100k_base tokens 257/263. Replies intentionally change to correct null values and retain source lines. No speed/token improvement is established; true cold, whole-agent/catalog/request/retry and real-device costs remain unmeasured. Heavy Qodo tests remained stopped.

<!-- gh-comment-id:6227449008 --> @morluto commented on GitHub (Oct 17, 2026): Addressed in `3d028dbc7`. The parser now owns declaration presence, the normalized object-reference value, and its original source line. Explicit `null` therefore becomes JSON null without being confused with an omitted declaration. The provider bases fallback on declaration presence; explicit absence does not cause another section read. `observed_lines` now preserves full named declaration lines (including indentation), making missing individual fields distinguishable from explicit absence. Named object identity values remain strings. This follows the release-tagged [Android DisplayContent dump producer](https://raw.githubusercontent.com/aosp-mirror/platform_frameworks_base/android-14.0.0_r1/services/core/java/com/android/server/wm/DisplayContent.java), which prints the two references directly. Selection among multiple display blocks remains separate from this fix. Validation on Node 24.18.0: - New parser/provider controls before repair: 4 failed, 76 passed. - Final 86 focused tests passed, including public CLI and production MCP with the actual advertised input/output schemas. Controls cover explicit-null windows/displays, named objects, omitted declarations, duplicate declarations, preserved source lines, and exact shell command counts. - Docs checks and five README conformance tests passed. Fake ADB only; no physical-device verification or full CI outcome is claimed. Bounded completed CLI comparison against immediate parent `bfb7ed0bd`: two alternating warm repeats/build plus separate first processes. Warm medians baseline/candidate: elapsed 0.35554/0.35468 s; CPU 0.54808/0.55076 s; sampled process-family RSS 352894976/350420992 bytes; raw reply cl100k_base tokens 257/263. Replies intentionally change to correct null values and retain source lines. No speed/token improvement is established; true cold, whole-agent/catalog/request/retry and real-device costs remain unmeasured. Heavy Qodo tests remained stopped.
Author
Owner

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

Closing as addressed on main by 3d028dbc7. Explicit mCurrentFocus=null and mFocusedApp=null now become JSON null while retaining the original declaration lines and distinguishing explicit absence from omitted declarations. I verified the committed source and regression coverage; I have not rerun the runtime reproduction.

<!-- gh-comment-id:6244119753 --> @coygeek commented on GitHub (Oct 18, 2026): Closing as addressed on `main` by [3d028dbc7](https://github.com/morluto/rea/commit/3d028dbc74a465f6f8df1813a931fbb5362e7dc3). Explicit mCurrentFocus=null and mFocusedApp=null now become JSON null while retaining the original declaration lines and distinguishing explicit absence from omitted declarations. I verified the committed source and regression coverage; I have not rerun the runtime reproduction.
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#2394
No description provided.