Skip to content

Device runtime: run pushed Codename One apps on a phone - #5561

Open
shai-almog wants to merge 173 commits into
masterfrom
device-runtime
Open

Device runtime: run pushed Codename One apps on a phone#5561
shai-almog wants to merge 173 commits into
masterfrom
device-runtime

Conversation

@shai-almog

@shai-almog shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Adds a device runtime: install one app on a phone, then push a project to it
from your IDE and watch it run natively in seconds. A third way to run a
Codename One app, alongside the simulator and a cloud device build.

Pushed classes are interpreted on the device against the framework already
compiled into it. Nothing is built, signed or installed between edits — the
edit-run loop measured 2.8 seconds end to end.

Try it

# install ~/cn1-device-runtime.apk on a phone (11MB, no native libs, any arch)
cd scripts/devruntime-ide-project
mvn -Ppush-lan package

The desktop finds the phone on the local network, shows a six-digit pairing
code you type once, and from then on it is edit-and-run. --device <address>
is there for networks that block a scan.

What is here

CodenameOne/src/com/codename1/interp/ the interpreter
Ports/{Android,iOSPort} per-platform linkers, iOS native bridge
vm/ByteCodeTranslator bundle writer, lambda desugaring, DevicePush tool
scripts/cn1-device-runtime/ the runtime app itself
scripts/devruntime-ide-project/ the project you open in an IDE
scripts/devruntime-probes/ 20 programs that found the defects worth knowing about
docs/developer-guide/Device-Runtime.asciidoc how and why

Decisions worth reviewing

Shims are generated over the whole API, never curated. A hand-maintained
list is a promise that applications only subclass what somebody anticipated, and
its failure mode is not an error message but an override that is silently never
called. The generator fails the build rather than pruning what will not compile
— a compile-and-drop loop once silently ate Interp_ui_Form.

Native-heavy subsystems are excluded from the shim set (ai, ar,
camera, surfaces, car, health, …). A shim is a compiled reference to the
class it extends, which is exactly what the build scans to decide what to link,
so generating the full API pulled 300MB of ML Kit, ARCore and CameraX natives
into an app that calls none of them. Cost: those types cannot be subclassed by
pushed code; calling them degrades to isSupported() == false, which is the
runtime's existing contract for a cn1lib without its native half.

iOS keeps shims rather than runtime vtable synthesis. Synthesis would make
14 more types extensible on iOS only, and Android cannot follow — so the usable
capability, the intersection, does not move. InterpHostVtableSynthesisIntegrationTest
stays for the day that changes.

synchronized uses the real object monitor, not a private lock table, which
is what makes wait/notify work.

Framework fixes that fell out

  • AndroidImplementation.getHostOrIP() returned dummy0's IPv6 link-local
    instead of a usable IPv4 — affects any caller.
  • CodenameOneImplementation.getResourceAsStream gained a local-resource hook,
    so a pushed program's theme.res is found by Resources.openLayered, which
    never passes through Display.

Verification

4798 core · 506 translator · 52 interpreter · SpotBugs 0 · 20-program device
battery green on an Android emulator and the iOS simulator, including a
four-file, three-package app entered through Lifecycle rather than main.

Every probe exists because something plausible turned out not to work; the
README records which defect each was written for.

Review rounds

Eleven findings from codex, all real, all fixed and each answered on its thread.
The two that mattered most:

  • Pairing handed out a bearer token. The peer id travelled in the clear on
    every push and never rotated, so one captured frame authorised arbitrary code
    on that phone forever. v2 is gone rather than deprecated. v3 derives a 256-bit
    secret on both ends from (typed code, peerId, deviceId) — never transmitted —
    and every connection answers a fresh challenge whose MAC covers the bundle.
    Authentication happens before the approval prompt, so nobody can raise dialogs
    on a stranger's phone until they tap Approve to stop them. What it still does
    not defeat is a passive observer of the pairing exchange itself, and the docs
    say so.
  • A failed class initializer left the class looking initialized, so later
    reads returned whatever half of it had been assigned. Four states and an owning
    thread now, per JLS 12.4.2.

Shipping it

.github/workflows/device-runtime-store.yml runs Mondays and on demand,
uploading to Play internal testing and TestFlight. It does not promote to
production and does not submit for review — a weekly automatic release would
put unread builds in front of the public and queue an iOS review every week
whether anything changed or not. Promotion stays one deliberate command.

Without credentials the job names the missing secrets and stops rather than
publishing half a release; none exist yet, so today it is a no-op that says so.

Listing text is in fastlane's layout (scripts/cn1-device-runtime/fastlane/) so
supply and deliver consume it directly, with store/privacy.md for both
stores' data forms and store/README.md for the secrets, the pre-submission
checklist and the review-risk assessment.

The compliance point that matters: this app runs code it did not ship with,
which is Guideline 2.5.2 — permitted for tools that develop or test code, and
only while the source is "completely viewable and editable by the user". The
runtime refuses to load a bundle whose sources it lacks, and shows them under
View source. Removing that screen makes the app unsubmittable, which is why
the code says so where the screen is defined.

Not done

NativeLookup stubbing covers the Java half of a cn1lib; the native half
reports unsupported. Resource push covers theme.res, CSS and images.

Screenshots for both stores, the Play content rating questionnaire, Apple's
privacy manifest and the console listings themselves are human steps, listed in
store/README.md.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ddc43de0d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/device-runtime-store.yml Outdated
Comment thread CodenameOne/src/com/codename1/interp/InterpRuntime.java Outdated
Comment thread vm/ByteCodeTranslator/src/com/codename1/tools/translator/InterpBundleWriter.java Outdated
Shai Almog and others added 3 commits August 17, 2026 15:49
Codename One apps run two ways today: the JavaSE simulator, which is not a
device, or a cloud device build, which costs minutes per iteration. This adds a
third: install one app on a phone, and from then on push a project to it from an
IDE and watch it run natively in seconds.

The app is not a shell around a compiled build. Pushed classes are interpreted
on the device against the framework already compiled into it, so nothing is
built, signed or installed between edits.

How the pieces fit
------------------

com.codename1.interp is the interpreter: one interpreted frame per real frame,
so Display.invokeAndBlock and every blocking idiom built on it still work. A
per-thread fuel counter bounds runaway code, and the budget is per entry into
the interpreter rather than per session -- measuring it per session kills every
callback that arrives later than the budget, which in an application whose whole
life is callbacks is every button press.

Interpreted classes reach the framework through InterpLinker: invoke thunks on
iOS, reflection on Android. A linker must dispatch on the receiver's class, not
the call site's declared type -- list.add(x) names java.util.List, and resolving
from there finds AbstractList.add, whose body throws.

Extending a framework class needs an object the framework accepts, which neither
platform can define at run time. Generated shims provide it: every public,
non-final, constructible class and every public interface the device exposes,
derived by scanning the framework jar and codenameone-java-runtime rather than
curated. A hand-maintained list is a promise that applications only subclass
what somebody anticipated, and its failure mode is not an error but an override
that is silently never called.

Lambdas and method references are rewritten into real classes when the bundle is
written, since neither target has a runtime invokedynamic. Enums are answered by
the interpreter, java.lang.Enum having no shim and needing none.

Store compliance is built in rather than bolted on: the runtime refuses to load
a bundle whose sources it cannot show, and shows them.

Verified
--------

4798 core tests, 506 translator tests, 43 interpreter tests, SpotBugs at zero,
and a 20-program device battery (scripts/devruntime-probes) passing on both an
Android emulator and the iOS simulator -- including a four-file, three-package
application entered through Lifecycle rather than main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Listing text in fastlane's layout so supply and deliver can consume it, a
privacy statement for both stores' data forms, and a scheduled workflow.

The weekly job uploads to Play internal testing and TestFlight. It does not
promote to production and does not submit for App Store review, which is a
decision rather than an omission: a weekly automatic release would put unread
builds in front of the public and queue an iOS review every week whether or not
anything changed. Promotion stays one command, taken deliberately.

Without publishing credentials the job reports which secrets are missing and
stops, rather than publishing half a release. None of them exist yet.

The review risk is written down rather than discovered later. This app runs code
it did not ship with, which is squarely Guideline 2.5.2 -- permitted for tools
that develop or test code, and only while the source stays viewable and editable
on the device. That is why the runtime refuses a bundle it cannot show the
source for. 4.7.2 is the sharper edge and the argument to make is that this is
point to point developer tooling rather than a mini-app platform.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ate every push

Three things the review asked for, and two defects found on the way.

The interpreter moves from com.codename1.interp to com.codename1.impl.interp.
It is an implementation detail of one app, not public API, and the impl
hierarchy is what keeps it out of the javadoc. Note the package name is not only
a Java name: ParparVM's dead-code pass recognises the runtime's own classes by
their C-mangled prefix, so Parser.isLoadBearingForInterp moved with it. Missing
that would have stubbed out InterpRuntime.run in an interp-host build, which
fails by succeeding -- every pushed program "runs" instantly and executes
nothing.

The ~1000 generated shims leave git. They are a mechanical function of the
framework jar, so the build generates them: a tools module builds the
generator, exec-maven-plugin runs it into target/generated-sources/shims, and
build-helper adds that as a source root.  scripts/generate-interp-shims.sh
keeps the three properties the build takes on faith -- every shim compiles, the
load-bearing ones exist, generating twice is identical -- and now asserts them
against a scratch tree instead of writing into src.

Pairing no longer hands out a bearer token. v2 authorised a push with a peer id
sent in the clear, so capturing one frame on a LAN meant pushing arbitrary code
to somebody's phone forever. v3 derives a 256-bit secret on both ends from the
typed code, the peer id and the device id -- never transmitted, 20k HMAC
iterations so grinding six digits costs something -- and every connection
answers a fresh challenge whose MAC covers the bundle. Authentication happens
before the approval prompt, so nobody can raise dialogs on a stranger's phone
until they tap Approve to stop them. What this still does not defeat is a
passive observer of the pairing exchange itself, which the docs now say plainly.
There are two implementations of the derivation, since ParparVM has no
javax.crypto; InterpPairingSecretTest runs both and compares.

Also fixed:

- A class initializer that threw left the class marked initialized, so later
  reads returned whatever half of it had been assigned. Four states and an
  owning thread now, per JLS 12.4.2.
- Sources were keyed by file name, so two Util.java in different packages
  collided and the runtime refused the program with "missing the source file
  Util.java" for a file it had been handed. Keyed by package now.
- The iOS release job resolved ExportOptions.plist relative to the generated
  Xcode project, which is not where it lives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check-copyright-headers gates every added source file, and the probes and the
IDE sample are ours -- not third-party, so the exclusions file (which is for
provenance, and rejects anything else) is the wrong place for them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

ghost commented Aug 17, 2026

Copy link
Copy Markdown

💡 Codex Review


P1 Badge Register the Android linker before checking support

On every Android launch this support check is false because no code installs the newly added InterpAndroidLinker: a repository-wide search finds InterpPlatform.register(...) only in IOSImplementation. Consequently DeviceRuntimeApp.init() returns without starting either transport and the Android runtime app cannot accept any pushed program; register an InterpAndroidLinker during Android port initialization.


final InterpRuntime rt = new InterpRuntime(bundle, InterpPlatform.getLinker(), factory);
factory.attach(rt);
runtime = rt;

P2 Badge Stop the previous program before replacing its runtime

When the normal “Push again … to replace it” workflow loads a second bundle, this assignment discards the service's reference to the previous runtime without requesting cancellation or invoking the previous Lifecycle.stop()/destroy(). Programs that registered global listeners, timers, network callbacks, or worker threads therefore continue executing alongside the replacement, and after this overwrite the service can no longer stop them.


if (!send(payload, port, peerId, false) && rejectedAsUnpaired()) {

P2 Badge Propagate failed LAN pushes as process failures

For an already-paired LAN push, send() returns false when the user denies approval, authentication fails, or the device rejects/runs the bundle unsuccessfully; unless the message contains “not paired,” this condition falls through and main() exits with status 0. The documented Maven push-lan profile therefore reports BUILD SUCCESS for a failed deployment, which also prevents scripts and IDE integrations from detecting the failure.


synchronized (found) {
if (found[0]) {
return;
}
found[0] = true;
foundAt[0] = candidate;
}
handle(is, os, false);

P2 Badge Validate a discovered peer before remembering its address

If any unrelated service happens to accept this port during the subnet sweep, the callback marks it as found before handle() validates the protocol magic. The sweep then persists that address, and subsequent dial attempts likewise treat a successful TCP connection as served even when the peer never sends a runtime frame, so discovery can remain stuck on the wrong machine; only publish found/foundAt after a valid handshake.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions

ghost commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3118d715e7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cn1-push.sh Outdated
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java Outdated
Comment thread .github/workflows/device-runtime-store.yml Outdated
Shai Almog and others added 2 commits August 17, 2026 21:15
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three were real: a SecureRandom built per call (worse and slower than one
seeded once, and what it generates is the pairing code), two Files.createDirectories
calls on a getParent() that SpotBugs cannot prove non-null, and an
ExecutorService.submit whose Future was never going to be read -- execute()
says what the scan actually wants.

The other two are recorded in spotbugs-exclude.xml with their reasons: a
command-line tool exits, and a failure while enumerating this machine's
interfaces must be answered with 'no device found' rather than by killing the
push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

ghost commented Aug 17, 2026

Copy link
Copy Markdown

💡 Codex Review

ShimObjectFactory factory = new ShimObjectFactory();
final InterpRuntime rt = new InterpRuntime(bundle, InterpPlatform.getLinker(), factory);
factory.attach(rt);
runtime = rt;

P1 Badge Tear down the previous runtime before replacing it

When a second bundle is pushed, this assignment only drops the service's reference to the previous runtime; it neither requests cancellation nor invokes any lifecycle cleanup. Peers, background threads, timers, and framework listeners retain references to the old runtime, so the supposedly replaced application can continue executing and mutate the UI or shared resources while the new application runs. Add a runtime deactivation/cleanup path and call it before publishing the replacement.


if ("toString".equals(name) && args.length == 0) {
return io.toString();
}
return NOT_OBJECT_METHOD;

P2 Badge Route Object monitor methods to interpreted-object monitors

For a peerless interpreted object, calls inherited from Object are handled here, but wait, notify, and notifyAll fall through to NOT_OBJECT_METHOD and ultimately raise AbstractMethodError. Consequently ordinary code such as synchronized (lock) { lock.wait(); }, where lock is a pushed POJO, cannot use Java monitor coordination even though MONITORENTER successfully acquired that same InterpObject; dispatch these methods against the monitor used by the interpreter.


for (File f : kids) {
if (f.isDirectory()) {
addSourceTree(f);
} else if (f.getName().endsWith(".java")) {
String text = new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8);
addSource(sourceKey(packageOf(text), f.getName()), text);

P2 Badge Include Kotlin files in pushed source bundles

When compiled output contains Kotlin classes, even explicitly passing a Kotlin source directory to --source cannot produce a valid bundle because this traversal ignores every .kt file. The reader later requires the SourceFile entry (for example Foo.kt) for each carried class and rejects the bundle as missing source, so Kotlin Codename One applications cannot be pushed; collect Kotlin sources and ensure the default project discovery also includes src/main/kotlin.


if ("com/codename1/system/Lifecycle".equals(cn.superName)) {
lifecycle = cn.name;
}

P2 Badge Discover Lifecycle subclasses through the class hierarchy

Entry-point discovery recognizes only classes whose immediate superclass is Lifecycle. If an application class extends a project-defined base lifecycle, this either reports no entry point or selects the base class itself (often abstract) instead of the concrete application, even though InterpRuntime.extendsHost() can execute an indirect subclass once selected. Resolve the collected superclass graph and choose the concrete transitive Lifecycle subclass.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4d7ad9bda

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vm/ByteCodeTranslator/src/com/codename1/tools/translator/DevicePush.java Outdated
Comment thread vm/ByteCodeTranslator/src/com/codename1/tools/translator/DevicePush.java Outdated
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java Outdated
@github-actions

ghost commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Cloudflare Preview

Seven findings, all real.

The interpreter:

- A class literal for a pushed type puts an InterpClass on the stack, because
  there is no host class object to hand back -- and then the bytecode goes on
  calling java.lang.Class methods on it, which no linker can serve. The part of
  Class that means anything here (naming, identity, isInterface, isInstance,
  getSuperclass) is answered by the interpreter; anything else is refused by
  name rather than answered wrongly.
- `new Entry[1][]` names its component `[LEntry;`, not `Entry`, so the
  bundle-membership test missed it and asked the host loader for a class only
  the bundle has. It looks through the brackets now, and multianewarray builds
  the nested Object[] itself rather than delegating.
- JLS 12.4.1: initializing a class initializes the superinterfaces that declare
  a default method. Only those -- initializing all of them would run
  initializers Java never runs, which is as wrong as running them late.

The push tool:

- The Lifecycle entry point was chosen by direct superclass only, so a project
  whose app extends its own BaseApp entered BaseApp: an abstract class that was
  never meant to be instantiated. It walks the hierarchy now and takes the
  deepest concrete descendant.
- A subnet scan treated any host that accepted TCP on the port as the device,
  and then failed the push against it while the real device sat unqueried.
  There is a PING frame now; only an answer in our own protocol wins.
- cn1-push.sh still spoke v2, which nothing accepts any more. Its paired mode
  is gone rather than ported: it is a loopback helper, and pushing to a phone
  over Wi-Fi is DevicePush's job. A third copy of the derivation in a shell
  script would only drift from the two that have to agree.

The release workflow now checks every secret the job will consume, not the two
that name the store, so a half-configured store says so in preflight instead of
half an hour later in the signing step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1356d055bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java Outdated
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java Outdated
Comment thread vm/ByteCodeTranslator/src/com/codename1/tools/translator/DevicePush.java Outdated
Comment thread CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java Fixed
CLDC11 keeps AbstractMethodError's constructors package-private, so the
framework cannot throw one with a message and the Ant leg would not compile.
IncompatibleClassChangeError carries the message, and a message naming the
method is worth more here than the exactly right type.

The three inline source blocks in the device runtime chapter move into
docs/demos and are included by tag, which is what the guide validator asks of
every other chapter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47fd057b7e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CodenameOne/src/com/codename1/impl/interp/InterpObject.java
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java Outdated
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java Outdated
The interpreter's depth cap throws one, and it is the right type: ParparVM's
java.lang has StackOverflowError and so does every JVM the simulator runs on.
It was simply missing from this compile-time stub, so the Ant leg could not
compile the framework while the Maven leg could -- the two disagree because
only the Ant build puts CLDC11 on the bootclasspath.

Reproducing that locally needs the same -bootclasspath; compiling core and
CLDC11 together against a full JDK resolves java.lang from the JDK and reports
nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

ghost commented Aug 17, 2026

Copy link
Copy Markdown

💡 Codex Review

@Override
public void init(Object m) {
// NOTE: Do not explicitly set the PlayServices instance to anything other than

P1 Badge Register the Android linker during port initialization

On every Android runtime build, DeviceRuntimeService.isSupported() requires InterpPlatform.isAvailable(), but the Android initialization path never calls InterpPlatform.register(new InterpAndroidLinker()); a repository-wide search finds no other construction of InterpAndroidLinker, while IOSImplementation.init() performs the corresponding registration. Consequently startDialer() always returns false on Android with “no interpreter bindings,” so the newly added Android device runtime cannot accept any pushes.


f.pushRef(isInterpretedLeaf(comp)
? new Object[count]
: linker.newArray(comp.startsWith("[") ? comp : "L" + comp + ";", count));

P2 Badge Preserve interpreted array component types

When the leaf type is interpreted, allocating every reference array as a plain Object[] discards its runtime component type. For example, after Sub[] a = new Sub[1]; Base[] b = a;, storing new Base() through b must throw ArrayStoreException, but AASTORE later writes unconditionally into this Object[], so the invalid value is accepted and the array is silently corrupted. Retain component metadata or otherwise validate each store against the allocated array type.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@shai-almog

ghost commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

ghost commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

The chapter was written before the rebase brought in the vale gate and had
never been through it: 141 findings, all in this one file. The bulk is the
guide's house style of contractions. The rest is adverbs that carried no
weight, two sentences opening with 'So', and three quotations of literal text
-- a runtime message, a device dialog, Google Play's policy -- where moving the
period inside the quotes would misquote the source, so those carry a vale-skip
naming the reason.

Where an adverb was load-bearing, the sentence says the thing instead: 'which
silently dropped Runnable' is now 'which dropped Runnable with no diagnostic'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a54e8f350

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/iOSPort/src/com/codename1/impl/ios/InterpIOSSymbols.java Outdated
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java Outdated
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java Outdated
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java
The rebase reconstruction rewrote the file with LF where master has CRLF, so a
63-line change showed up as a 14,000-line rewrite. That is not only noise:
CodeQL reports alerts for code a PR changed, and a whole-file diff re-reported
twelve alerts that master already has and this branch did not introduce.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f86bfb26ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cn1-push.sh Outdated
Comment thread scripts/cn1-push.sh Outdated
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java
The chapter used British spellings in a US-English guide -- which the
cross-document coherency rule catches, not just the dictionary -- and a
vocabulary LanguageTool has never heard of. Spellings are now US; the
vocabulary (vtable, clazz, dex, desugar, devirtualize, supertype, cmake,
thebaselab) is in the accept list with a line saying what each one is.

Two sentences were rephrased rather than allowlisted: LanguageTool reads
'An interpreted X has to be an object...' as a typo for 'and' once the code
spans are stripped, and the rule is right that the sentence was hard to parse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79950c65f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java
Comment thread CodenameOne/src/com/codename1/impl/interp/InterpRuntime.java
Codex flagged two P2s:

- LDC of a string constant pushed the bundle's own String reference,
  which is only deduplicated within the bundle and never enters the
  VM's intern pool. `ldc` semantics require the canonical interned
  instance, so `"hi" == "hi"` came out false when one side was a
  pushed literal and the other host-produced -- the failure that
  identity-sensitive code (switch on intern, String.equals fast
  path built on ==) reads as spurious. Intern before pushing.

- iOS methodId's class-chain walk returned an inherited private
  method's id without checking the private flag the sidecar
  records, so `B extends A implements I` with `A.private m()` and
  `I.m()` dispatched to `A.m` instead of the JVM's IllegalAccessError
  route. Track the private flag as its own set (privateMethods),
  skip an inherited match found on a superclass, and reject the
  directly-declared case at invokeVirtual too (invokeSpecial keeps
  private access, since super/init/nestmate are its whole job).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 000c53aa75

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

codename1.arg.ios.newStorageLocation=true
codename1.arg.ios.NSHealthShareUsageDescription=Used by the CI smoke test to verify the com.codename1.health native bridge compiles. The app never reads real health data.
codename1.arg.ios.NSHealthUpdateUsageDescription=Used by the CI smoke test to verify the com.codename1.health write path compiles. The app never writes real health data.
codename1.arg.ios.uiscene=true

ghost Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enable interpHost in the release app settings

When the signed IPA is produced through the documented ios-device-release cloud path, these project settings never pass ios.interpHost=true, so the translator omits the invoke thunks and symbol table and DeviceRuntimeService.isSupported() returns false. A repo-wide search found the flag only in run-device-runtime-ios.sh, meaning the local simulator build works while the TestFlight/release build cannot run any pushed code; persist codename1.arg.ios.interpHost=true with the other iOS settings.

Useful? React with 👍 / 👎.

Codex noticed that a signed IPA produced through the cloud
`ios-device-release` path would have `DeviceRuntimeService.isSupported()`
answer false, because `codenameone_settings.properties` never carried
`codename1.arg.ios.interpHost=true`. Only `run-device-runtime-ios.sh`
did, so the simulator worked and the release build was inert: the
translator omitted the invoke thunks and the sidecar, and there is no
runtime knob that puts them back. Persist the flag with the rest of the
iOS args so the customer cloud path (per store/README.md) produces a
build that can actually run pushed code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3ee457311a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (f == null) {
throw new NoSuchFieldException(key);
}
f.setAccessible(true);

ghost Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject host fields that became private

When pushed code was compiled while a host field was accessible but the installed framework changed the same field to private without changing its name or descriptor, this unconditional setAccessible(true) lets every GET/PUT accessor continue reading or writing it. The JVM would reject that stale access with IllegalAccessError; validate field accessibility before caching it. The JavaSE reflection linker mirrors this bypass.

Useful? React with 👍 / 👎.

Constructor ctor = ctorCache.get(key);
if (ctor == null) {
ctor = c.getDeclaredConstructor(paramTypes(descriptor));
ctor.setAccessible(true);

ghost Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject host constructors that became private

When a bundle was compiled against a public host constructor that is private in the installed framework revision, getDeclaredConstructor() still finds it and this line bypasses the new visibility, so new Host(...) succeeds where JVM constructor resolution would raise IllegalAccessError. Check constructor accessibility before making it invocable; the JavaSE linker contains the same path.

Useful? React with 👍 / 👎.

Codex spotted the same "installed framework made it private" gap in
two remaining places both linkers rely on:

- lookupField cached whatever resolveField returned and called
  setAccessible(true) unconditionally. A host field the installed
  framework has since made private slipped through -- the get/set
  succeeded on stale bindings the JVM would refuse with
  IllegalAccessError. Field access has no invokespecial equivalent,
  so refuse a private declaration outright.
- construct did the same for constructors: getDeclaredConstructor
  found the private ctor, setAccessible(true) made new Host(...) run
  on a stale binding. Same guard.

Two tests cover each new refusal. All three linkers (Android, JavaSE,
ReflectionInterpLinker) get the fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0a38c708f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2580 to +2583
// Object parameter loses the token itself: `list.add(Pushed.class)`
// would store Object.class, and reading it back would not equal the
// literal the program still holds. A host that took it as Object never
// needed a Class in the first place -- it is storing a reference.

ghost Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve class-literal text across host Object calls

When a pushed class literal is passed to a host method whose parameter is Object, such as System.out.println(Thing.class), String.valueOf(Thing.class), or StringBuilder.append(Thing.class), this conversion deliberately leaves the internal InterpClass token unchanged. The host then invokes InterpClass.toString() directly rather than the interpreter's classCall() handling; because InterpClass does not override toString(), it produces com.codename1.impl.interp.InterpClass@... instead of the JVM result class Thing. Preserve token identity for collections while giving host-side Object consumers the class-literal representation.

Useful? React with 👍 / 👎.

Codex noticed that a pushed class literal (`Pushed.class`) handed to a
host method's Object parameter -- `System.out.println(...)`,
`String.valueOf(...)`, `StringBuilder.append(...)` -- was passed
through as the raw InterpClass token so its identity would survive
storage in a `List<Class>`. The host's toString() call then hit the
default Object.toString, printing
`com.codename1.impl.interp.InterpClass@1a2b3c` instead of `class Foo`.
The interpreter's classCall handler already returns the JVM-shaped
literal when interpreted code invokes toString on the token; add the
same shape as an override on InterpClass so a host consumer gets it
too, while identity (equals / hashCode via Object) stays untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 049fb922bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +329 to +330
if (!java && (t == '\n' || t == '\r')) {
break;

ghost Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep scanning continued Kotlin package directives

When a valid Kotlin package name wraps after a dot, such as package com.\nexample, Kotlin treats the newline as whitespace because the preceding . cannot terminate the directive. This branch nevertheless stops at the newline and returns com., so addSourceTree() stores the file under the wrong key and requireSourcesFor() rejects the emitted com/example/... class as missing its source. End the Kotlin scan only at a newline that can actually terminate the qualified identifier.

Useful? React with 👍 / 👎.

Codex noticed the Kotlin package scanner stopped at the first newline
even when the qualified identifier could not terminate there. Kotlin
treats a newline that follows a `.` as whitespace -- `package com.\n
example` is one name -- so the scanner returned `com.`, the source was
keyed at the wrong path, and the emitted `com/example/...` classes
were then refused as missing their source.

Skip a newline whose nearest non-whitespace predecessor is a dot; a CR
in front of the dot (Windows CRLF) is treated as line whitespace by a
small helper so the check works on either line-ending style.

Two tests cover LF and CRLF forms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e248c9e793

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +343 to +344
w.write("class\t" + (base + rank) + "\tarray" + rank + "__" + primNames[p]
+ "\t\t-1\t" + brackets + primLetter[p] + "\t\n");

ghost Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register primitive-array clazz objects

On iOS, when pushed code evaluates a primitive-array literal such as int[].class, this new sidecar row lets findClass("[I") resolve an ID, but InterpIOSLinker.classObject() then calls classObjectById(), whose native registry has no entry for any primitive-array clazz; the generated registration loop only covers reference arrays. The literal is therefore pushed as null, causing incorrect comparisons or an NPE on the first Class method call. Fresh evidence after the earlier sidecar-row report is that the rows now exist while their corresponding class_arrayN__JAVA_* objects remain unregistered.

Useful? React with 👍 / 👎.

Comment on lines +722 to +724
String id = hex(randomBytes(16));
createParent(f);
Files.write(f, id.getBytes(StandardCharsets.UTF_8));

ghost Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize first-time peer ID creation

When two Maven or IDE pushes start concurrently before ~/.codenameone/devruntime-peer exists, both processes pass the existence check, generate different IDs, overwrite the file, and return their own ID. Pairing secrets are derived from that ID, so at least one process can immediately authenticate with a secret belonging to the other persisted identity, or a later push can no longer use a pairing established by the losing process. Create the peer ID under an inter-process lock or with an atomic create-and-read-winner operation.

Useful? React with 👍 / 👎.

Shai Almog and others added 3 commits August 26, 2026 15:22
Codex flagged two P2s, both about earlier rounds landing rows / files
without wiring their consumers up:

- Round 125 added sidecar rows for primitive-array descriptors (`[I`,
  `[Z`, ..., `[[[D`) so `int[].class` resolves to a class id on iOS,
  but the id-to-clazz registry has no entry for any primitive-array
  class object -- `appendInterpArrayClassRegistrations` only emits
  registrations for reference arrays it can name per generated class.
  The literal was therefore pushed as null and the first Class method
  call NPE'd. Register all 24 primitive-array class objects
  (`class_arrayN__JAVA_TYPE`) from a single
  `__attribute__((constructor))` in cn1_globals.m, gated on
  CN1_ON_DEVICE_DEBUG the same way the debugger's class registry
  compiles.

- DevicePush.peerId did check-then-write on ~/.codenameone/
  devruntime-peer, so two Maven or IDE pushes starting concurrently
  before the file exists would both pass the check, generate
  different ids, race the write, and return ids that no longer
  identify each other -- pairing secrets derived from them would
  authenticate with a peer id the other side had already forgotten.
  Use CREATE_NEW (atomic create-if-absent on POSIX and Windows)
  and, on FileAlreadyExistsException, read what the winner wrote.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-fixes

# Conflicts:
#	.gitignore
#	CLAUDE.md
#	scripts/hellocodenameone/common/codenameone_settings.properties
Master added a compile-time check that every codename1.arg.<name> line
either has an annotation under CodenameOne/src/com/codename1/annotations/
buildhints or an entry in maven/build-hint-catalog. ios.interpHost is
already read by the ParparVM translator (BytecodeMethod#isInterpHost
gates the invoke thunks and the symbol sidecar the device runtime
loads at run time) and the cn1-device-runtime settings pass it, so
declare it in BuildHintsIos with the default the translator applies
when the hint is absent (false).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d5da4f328d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +643 to +645
if (explicitDevice != null) {
Socket direct = new Socket();
direct.connect(new InetSocketAddress(explicitDevice, port), 4000);

ghost Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the manual iOS path in the dial-out direction

When --device is used with a physical iOS device—particularly on IPv6, client-isolated Wi-Fi, or another network where subnet discovery fails—this branch always initiates a desktop-to-device connection. However, IOSImplementation.listenSocket() deliberately returns null, and DeviceRuntimeService.startListener() consequently does not expose this port on iOS, so the advertised manual fallback always ends in connection refused. Provide a device-side way to enter the desktop address and let the device dial out, or explicitly restrict this option to platforms that can listen.

Useful? React with 👍 / 👎.

Shai Almog and others added 2 commits August 28, 2026 17:19
Master's compile-time build-hint gate (d21a389) refuses a hint that
lives in both an @Android/@iOS annotation and codenameone_settings.
properties. HelloCodenameOne.kt already carries useAndroidX=Toggle.ON
and applicationQueriesSchemes=["cydia"] as annotations, so the two
lines the device-runtime "shim every native" block added for them
(useAndroidX, applicationQueriesSchemes) were caught as duplicates
and every hellocodenameone-common build failed at cn1-process-classes.
Nine CI jobs went red on that step alone: Android x3, Car Android
Auto, cross-build suite, screenshot-capture x64, build suite classes
x64, javascript-screenshots, and the downstream
cross-compiled screenshot-comment. Drop the duplicates; the comment
above the remaining licenseKey/messagingService pair now says the
annotation is the source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex flagged that --device <address> always initiates a
desktop-to-device connection, but iOS deliberately answers null from
IOSImplementation.listenSocket -- so an iPhone the user typed in
never accepts an incoming connection. The on-device UI already labels
that address `(dial-out only)`, but the desktop's error read as a
generic connection refusal. Wrap the connect(); on failure, say why
the address can't answer on iOS and point the user at the paired
flow (the runtime dials out; the desktop listens), letting the
underlying refusal escape as the cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1864ea3c61

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +497 to +500
Method m = lookupMethod(owner, name, descriptor);
if (!Modifier.isStatic(m.getModifiers())) {
throw new IncompatibleClassChangeError(owner + "." + name + " is not static");
}

ghost Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject private static methods at static call sites

When pushed code was compiled while a host static method was accessible but the installed framework has since made it private, this path validates only that the method is static and then invokes the declaration after lookupMethod() called setAccessible(true), so stale code executes a method the JVM would reject with IllegalAccessError. Fresh evidence after the earlier virtual-method fix is that the private check was added only to invokeVirtual(); invokeStatic() remains unchecked in the Android, JavaSE, and iOS linkers.

Useful? React with 👍 / 👎.

Comment on lines +203 to +210
int id = symbols.declaredMethodId(owner, "<init>", descriptor);
if (id < 0) {
throw new NoSuchMethodError(owner + ".<init>" + descriptor
+ " is not present in the installed app");
}
// A constructor thunk allocates its own receiver and returns it,
// which is why this passes no target and expects an object back.
return invokeWithId(id, descriptor, null, args, K_OBJECT);

ghost Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject private constructors in the iOS linker

On iOS, when a host constructor that was accessible at bundle compile time is private in the installed app, declaredMethodId() still returns its sidecar ID and this immediately invokes its generated thunk. The sidecar already records the private bit, so check symbols.isPrivateMethod(id) here and raise IllegalAccessError rather than allowing stale pushed code to instantiate the class.

Useful? React with 👍 / 👎.

Comment on lines +225 to +233
String[] p = split(table.substring(start, end));
if (p.length >= 5) {
String ownerName = (String)classNames.get(Integer.valueOf(p[1]));
if (ownerName != null) {
// Descriptor in the key. A rebuilt host that changed a
// field's type but kept the name would otherwise bind and
// the caller would read a primitive slot as an object
// reference (or the other way).
fieldIds.put(ownerName + "#" + p[3] + "#" + p[4], Integer.valueOf(p[2]));

ghost Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve iOS field access flags during symbol loading

When an installed iOS framework narrows a previously accessible field to private, the generated field and sfield rows include access flags as their sixth column, but this parser stores only the owner, name, descriptor, and ID. Consequently getField/setField and the static accessors have no way to reject the private field and directly access it through native offsets or generated accessors, whereas the JVM would raise IllegalAccessError; retain the flags and enforce them during field resolution.

Useful? React with 👍 / 👎.

Codex flagged three "now-private" gaps left over from earlier rounds:

- invokeStatic accepted a private static method across all three
  linkers (Android, JavaSE, ReflectionInterpLinker) and on iOS.
  The invokeVirtual fix covered only the instance path; a host
  static the installed framework has since made private slipped
  through the same setAccessible bypass. Reject it with the same
  IllegalAccessError, and mirror it on iOS by consulting the
  sidecar's isPrivateMethod flag.
- iOS constructors had no private check at all. If a host
  constructor accessible at bundle-compile is private now, the
  sidecar's declaredMethodId still returned the thunk and it ran.
  Same guard as the method paths.
- The iOS sidecar's field/sfield rows carry a JDWP access-flags
  column (the sixth), but the parser only kept owner/name/desc/id.
  A now-private host field could therefore be read or written
  through the generated native accessor with no gate. Store the
  private bit in a new privateFields set and consult
  isPrivateField in getStatic/setStatic/getField/setField --
  raising IllegalAccessError before the native call.

A test covers the new invokeStatic path; the constructor and field
cases don't fit the ProxyInterpObjectFactory harness (Proxy can only
proxy interfaces, so a host class with a private constructor never
gets a peer through the test factory) but the guards follow the same
shape as the invokeVirtual rejection tests already do.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 590ba68dd8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* documented.
*/
public void addSource(String fileName, String text) {
sources.put(fileName, text);

ghost Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject colliding source keys across roots

When two configured source roots contain files with the same package and filename, this unconditional put silently replaces the first source. This is valid for files containing differently named package-private classes (for example, two p/Common.java files producing p/A.class and p/B.class), and requireSourcesFor() subsequently accepts both classes because their shared key exists, even though only one class's source is actually viewable. Reject conflicting contents or preserve each class-to-source association instead of letting the last root win.

Useful? React with 👍 / 👎.

Codex noticed that addSource silently replaced a prior entry keyed by
the same package+filename, so two source roots each containing a
`p/Common.java` -- one declaring package-private class A, the other
declaring package-private class B -- would leave requireSourcesFor
happy with only one of the two texts in the bundle. The class whose
source got dropped ends up with a SourceFile attribute naming a file
whose contents are the other class's, which is exactly the guarantee
guideline 2.5.2 rests on. Refuse the second put when the contents
differ and let the writer die loudly; a bundle where one class's
source is really the other's cannot ship.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f72547ee7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cn1-push.sh
w.addSourceTree(sourceRoot);
// Everything that is not source: theme.res, CSS, images. Keyed by
// path relative to the tree, which is how an application loads them.
w.addResourceTree(sourceRoot);

ghost Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the conventional Maven resources directory

When this helper is invoked exactly as documented (scripts/cn1-push.sh src/main/java), sourceRoot is the Java source directory, so this call scans only src/main/java for resources. Files in the conventional and documented src/main/resources sibling—including theme.res, images, and application data—are therefore omitted from the bundle, causing pushed applications to use the runtime's theme or receive null when loading their resources. The helper should also detect and add the sibling resources directory.

Useful? React with 👍 / 👎.

Codex noticed cn1-push.sh, invoked as documented
(`scripts/cn1-push.sh src/main/java`), scanned only the Java root for
resources -- so `src/main/resources/theme.res`, images and JSON blobs
never entered the bundle and the pushed program wore the runtime's
theme. Same story for a Kotlin project's sibling `kotlin/` root, whose
sources would leave the runtime refusing the whole push as missing
source for the Kotlin classes. Detect both siblings on the source
root's parent when the root is a directory: add `resources/` as a
resource tree, and add `kotlin/` as both source and resource tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f1859552f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3879 to +3883
if (v instanceof NativeStub) {
// The cast the NativeLookup idiom always performs. The stub stands
// in for the interface it was asked for, and for NativeInterface
// above it.
InterpClass iface = ((NativeStub) v).iface;

ghost Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat native stubs as Object instances

When pushed code evaluates nativeStub instanceof Object, this special-case runs before the universal java/lang/Object branch and returns false because the interpreted native interface is not an interpreted subtype of the host Object class. NativeStub is nevertheless a non-null Java reference, so ordinary instanceof Object checks produce the opposite result from every JVM; handle Object before this branch or include it explicitly here.

Useful? React with 👍 / 👎.

Comment on lines +806 to +810
out.writeInt(staticFields.size());
for (FieldNode fn : staticFields) {
out.writeInt(intern(fn.name));
out.writeInt(intern(fn.desc));
out.writeInt(fn.access);

ghost Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve ConstantValue initializers for static fields

When a bundled class has a static field with a ConstantValue attribute and a consumer actually emits GETSTATIC—for example, bytecode produced by instrumentation or an incrementally compiled consumer from before the field became final—this serialization drops FieldNode.value. The reader consequently initializes the field to zero or null, and no <clinit> assignment exists for a constant-value field, so interpreted execution observes the wrong value; serialize and restore the constant alongside the field metadata.

Useful? React with 👍 / 👎.

Codex flagged two P2s:

- isInstanceOf's NativeStub branch answered first and returned false
  for `nativeStub instanceof java.lang.Object`, because the stub's
  interpreted interface does not list Object as a supertype. A
  NativeStub is still a Java reference, so every JVM answers true.
  Move the Object short-circuit above the NativeStub branch (and
  above every other check) since it applies universally.

- Static field rows never carried the ConstantValue attribute
  (JVMS 4.7.2), so a `static final int X = 42` read as 0 through
  GETSTATIC. javac normally inlines the constant, but bytecode from
  instrumentation or a caller compiled against an older non-final
  version still emits `getstatic C.X`. Preserve the constant in a
  small tag+value envelope on write, and restore it in the reader
  with descriptor-driven coercion (a boolean / byte / char / short
  ConstantValue is stored as an int per JVMS). Bump the bundle
  format to VERSION 5. Test covers int / long / String.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef4cbf6299

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +753 to +754
Files.write(f, id.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);

ghost Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish the initial peer ID atomically

When two first-time pushes start concurrently, CREATE_NEW makes file creation exclusive but does not make the subsequent write atomic: once the winner opens the new file, the loser can observe it at the initial Files.exists check or in this catch block and read an empty or partially written ID before the winner finishes. That process then derives and stores pairing state under an identity that is not the persisted 32-character ID. Fresh evidence after the prior race report is this remaining visibility window between creation and content publication; write to a temporary file and atomically publish it, or hold an inter-process lock through creation, writing, and loser reads.

Useful? React with 👍 / 👎.

// to queue behind this one.
releasePreAuth(length);
released = true;
if (!approveWhileWaiting(peerId, progress)) {

ghost Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize authenticated approval prompts

When a paired peer without an Always approval submits several valid pushes concurrently, every connection reaches this call independently after the pre-auth reservation has already been released. Unlike pairing prompts, approval prompts have no shared claim, so their callSeriallyAndWait tasks can open nested modal dialogs on the EDT (up to the accepted-connection cap), obscuring which bundle is being approved and making the runtime UI unusable until every prompt is dismissed. Reserve a single approval slot or coalesce concurrent approvals per peer before opening the dialog.

Useful? React with 👍 / 👎.

Codex flagged two P2s and CI caught one PMD violation from round 140:

- CREATE_NEW made the peer-id file's creation exclusive but not its
  write: a losing process could see the newly-created 0-byte file at
  Files.exists / Files.readAllBytes before the winner finished
  writing and derive pairing state under an empty id. Publish via
  temp-file + ATOMIC_MOVE so the file appears to a reader only when
  the id is already inside it, and treat an existing 0-byte file as
  a leftover to rewrite rather than a valid id. Fall through to a
  plain move on filesystems that refuse ATOMIC_MOVE.
- Concurrent authenticated pushes from a peer without an "Always"
  approval could each open a modal Approve dialog on top of the last
  because the pre-auth reservation was released before the approval
  fired. Serialize approveWhileWaiting on approvalLock so only one
  prompt is on screen at a time; a peer with Always still returns
  from DeviceRuntimePairing.approve without prompting, so the lock
  costs nothing on the fast path.

Also: round 140's readConstantValue declared `throws java.io.IOException`
even though IOException was already imported, and PMD's
UnnecessaryFullyQualifiedName rule refused the build; use the short
name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

ghost commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T04:49:53.939323Z af76ff8 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af76ff8db1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Files.write(tmp, id.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
try {
Files.move(tmp, f, StandardCopyOption.ATOMIC_MOVE);

ghost Sep 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent atomic moves from replacing the winning peer ID

When two first-time pushes publish concurrently on a typical Unix filesystem, ATOMIC_MOVE uses an atomic rename that replaces an existing destination even though REPLACE_EXISTING was omitted, so both moves can succeed and this FileAlreadyExistsException recovery never runs. Each process then returns its own ID while only the last ID remains on disk, making pairing established by the earlier process unusable. Fresh evidence after the prior peer-ID race fix is that the new atomic move itself does not provide create-if-absent semantics; use an inter-process lock or another publication mechanism that cannot replace the winner.

Useful? React with 👍 / 👎.

case 2: return Long.valueOf(in.readLong());
case 3: return Float.valueOf(in.readFloat());
case 4: return Double.valueOf(in.readDouble());
case 5: return b.strings[in.readInt()];

ghost Sep 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Intern ConstantValue strings when restoring static fields

When bytecode actually reads a static final String through GETSTATIC—for example, an incrementally compiled consumer that was built before the field became final—this returns the ordinary readUTF() instance from the bundle table. The field has no <clinit> assignment to replace it, so comparing it by identity with the same LDC literal can incorrectly return false, whereas JVM string constants are canonical interned instances. Fresh evidence after the earlier LDC interning fix is this newly added ConstantValue path, which bypasses the .intern() used by LDC_STRING.

Useful? React with 👍 / 👎.

@shai-almog

ghost commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 160 screenshots: 160 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 247 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 109ms / native 4ms = 27.2x speedup
SIMD float-mul (64K x300) java 87ms / native 7ms = 12.4x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 248.000 ms
Base64 CN1 decode 106.000 ms
Image encode benchmark iterations 100
Image createMask (SIMD off) 14.000 ms
Image createMask (SIMD on) 6.000 ms
Image createMask ratio (SIMD on/off) 0.429x (57.1% faster)
Image applyMask (SIMD off) 164.000 ms
Image applyMask (SIMD on) 140.000 ms
Image applyMask ratio (SIMD on/off) 0.854x (14.6% faster)
Image modifyAlpha (SIMD off) 132.000 ms
Image modifyAlpha (SIMD on) 120.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.909x (9.1% faster)
Image modifyAlpha removeColor (SIMD off) 161.000 ms
Image modifyAlpha removeColor (SIMD on) 195.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.211x (21.1% slower)

@shai-almog

ghost commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 300 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 83ms / native 10ms = 8.3x speedup
SIMD float-mul (64K x300) java 52ms / native 2ms = 26.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 167.000 ms
Base64 CN1 decode 93.000 ms
Base64 native encode 650.000 ms
Base64 encode ratio (CN1/native) 0.257x (74.3% faster)
Base64 native decode 288.000 ms
Base64 decode ratio (CN1/native) 0.323x (67.7% faster)
Base64 SIMD encode 55.000 ms
Base64 encode ratio (SIMD/CN1) 0.329x (67.1% faster)
Base64 SIMD decode 55.000 ms
Base64 decode ratio (SIMD/CN1) 0.591x (40.9% faster)
Base64 encode ratio (SIMD/native) 0.085x (91.5% faster)
Base64 decode ratio (SIMD/native) 0.191x (80.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 12.000 ms
Image createMask (SIMD on) 3.000 ms
Image createMask ratio (SIMD on/off) 0.250x (75.0% faster)
Image applyMask (SIMD off) 108.000 ms
Image applyMask (SIMD on) 95.000 ms
Image applyMask ratio (SIMD on/off) 0.880x (12.0% faster)
Image modifyAlpha (SIMD off) 51.000 ms
Image modifyAlpha (SIMD on) 74.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.451x (45.1% slower)
Image modifyAlpha removeColor (SIMD off) 66.000 ms
Image modifyAlpha removeColor (SIMD on) 54.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.818x (18.2% faster)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants